tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55
  56from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  57from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  58
  59import UniLogger as uLog  # Logger for TKSBrokerAPI
  60
  61
  62# --- Common technical parameters:
  63
  64PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  65uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  66uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  67uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  68
  69__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  70
  71CPU_COUNT = cpu_count()  # host's real CPU count
  72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  73
  74# --- Main constants:
  75
  76NANO = 0.000000001  # SI-constant nano = 10^-9
  77
  78
  79def NanoToFloat(units: str, nano: int) -> float:
  80    """
  81    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  82
  83    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  84
  85    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  86
  87    :param units: integer string or integer parameter that represents the integer part of number
  88    :param nano: integer string or integer parameter that represents the fractional part of number
  89    :return: float view of number
  90    """
  91    return int(units) + int(nano) * NANO
  92
  93
  94def FloatToNano(number: float) -> dict:
  95    """
  96    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  97
  98    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  99
 100    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
 101
 102    :param number: float number
 103    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 104    """
 105    splitByPoint = str(number).split(".")
 106    frac = 0
 107
 108    if len(splitByPoint) > 1:
 109        if len(splitByPoint[1]) <= 9:
 110            frac = int("{}{}".format(
 111                int(splitByPoint[1]),
 112                "0" * (9 - len(splitByPoint[1])),
 113            ))
 114
 115    if (number < 0) and (frac > 0):
 116        frac = -frac
 117
 118    return {"units": str(int(number)), "nano": frac}
 119
 120
 121def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 122    """
 123    Create tuple of date and time strings with timezone parsed from user-friendly date.
 124
 125    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 126
 127    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 128    An error exception will occur if input date has incorrect format.
 129
 130    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 131    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 132    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 133    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
 134
 135    Also, you can use keywords for start if `end=None`:
 136    `today` (from 00:00:00 to the end of current day),
 137    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 138    `week` (-7 day from 00:00:00 to the end of current day),
 139    `month` (-30 day from 00:00:00 to the end of current day),
 140    `year` (-365 day from 00:00:00 to the end of current day),
 141
 142    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 143             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 144             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 145    """
 146    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 147    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 148    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 149
 150    # time between start and the end of the current day:
 151    if start is None or start.lower() == "today":
 152        pass
 153
 154    # from start of the last day to the end of the last day:
 155    elif start.lower() == "yesterday":
 156        s -= timedelta(days=1)
 157        e -= timedelta(days=1)
 158
 159    # week (-7 day from 00:00:00 to the end of the current day):
 160    elif start.lower() == "week":
 161        s -= timedelta(days=6)  # +1 current day already taken into account
 162
 163    # month (-30 day from 00:00:00 to the end of current day):
 164    elif start.lower() == "month":
 165        s -= timedelta(days=29)  # +1 current day already taken into account
 166
 167    # year (-365 day from 00:00:00 to the end of current day):
 168    elif start.lower() == "year":
 169        s -= timedelta(days=364)  # +1 current day already taken into account
 170
 171    # -N days ago to the end of current day:
 172    elif start.startswith('-') and start[1:].isdigit():
 173        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 174
 175    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 176    else:
 177        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 178        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 179
 180    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 181    s = s.strftime(TKS_DATE_TIME_FORMAT)
 182    e = e.strftime(TKS_DATE_TIME_FORMAT)
 183
 184    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 185
 186    return s, e
 187
 188
 189class TinkoffBrokerServer:
 190    """
 191    This class implements methods to work with Tinkoff broker server.
 192
 193    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 194
 195    About `token`: https://tinkoff.github.io/investAPI/token/
 196    """
 197    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 198        """
 199        Main class init.
 200
 201        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 202        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 203                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 204        :param useCache: use default cache file with raw data to use instead of `iList`.
 205                         True by default. Cache is auto-update if new day has come.
 206                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 207        :param defaultCache: path to default cache file. `dump.json` by default.
 208        """
 209        if token is None or not token:
 210            try:
 211                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 212                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 213
 214            except KeyError:
 215                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 216                raise Exception("Token required")
 217
 218        else:
 219            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 220            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 221
 222        if accountId is None or not accountId:
 223            try:
 224                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 225                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 226
 227            except KeyError:
 228                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 229
 230        else:
 231            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 232            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 233
 234        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 235        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 236
 237        Latest version: https://pypi.org/project/tksbrokerapi/
 238        """
 239
 240        self.aliases = TKS_TICKER_ALIASES
 241        """Some aliases instead official tickers.
 242
 243        See also: `TKSEnums.TKS_TICKER_ALIASES`
 244        """
 245
 246        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 247
 248        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 249
 250        self.ticker = ""
 251        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 252
 253        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 254        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 255
 256        See also: `SearchByTicker()`, `SearchInstruments()`.
 257        """
 258
 259        self.figi = ""
 260        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 261
 262        See also: `SearchByFIGI()`, `SearchInstruments()`.
 263        """
 264
 265        self.depth = 1
 266        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 267
 268        See also: `GetCurrentPrices()`.
 269        """
 270
 271        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 272        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 273
 274        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 275        """
 276
 277        uLogger.debug("Broker API server: {}".format(self.server))
 278
 279        self.timeout = 15
 280        """Server operations timeout in seconds. Default: `15`.
 281
 282        See also: `SendAPIRequest()`.
 283        """
 284
 285        self.headers = {
 286            "Content-Type": "application/json",
 287            "accept": "application/json",
 288            "Authorization": "Bearer {}".format(self.token),
 289            "x-app-name": "Tim55667757.TKSBrokerAPI",
 290        }
 291        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 292
 293        See also: `SendAPIRequest()`.
 294        """
 295
 296        self.body = None
 297        """Request body which send to broker server. Default: `None`.
 298
 299        See also: `SendAPIRequest()`.
 300        """
 301
 302        self.moreDebug = False
 303        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 304
 305        self.historyFile = None
 306        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 307
 308        See also: `History()`.
 309        """
 310
 311        self.htmlHistoryFile = "index.html"
 312        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 313
 314        See also: `ShowHistoryChart()`.
 315        """
 316
 317        self.instrumentsFile = "instruments.md"
 318        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 319
 320        See also: `ShowInstrumentsInfo()`.
 321        """
 322
 323        self.searchResultsFile = "search-results.md"
 324        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 325
 326        See also: `SearchInstruments()`.
 327        """
 328
 329        self.pricesFile = "prices.md"
 330        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 331
 332        See also: `GetListOfPrices()`.
 333        """
 334
 335        self.infoFile = "info.md"
 336        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 337
 338        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 339        """
 340
 341        self.bondsXLSXFile = "ext-bonds.xlsx"
 342        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 343        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 344
 345        See also: `ExtendBondsData()`.
 346        """
 347
 348        self.calendarFile = "calendar.md"
 349        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 350        
 351        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 352
 353        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 354        """
 355
 356        self.overviewFile = "overview.md"
 357        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 358
 359        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 360        """
 361
 362        self.overviewDigestFile = "overview-digest.md"
 363        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 364
 365        See also: `Overview()` with parameter `details="digest"`.
 366        """
 367
 368        self.overviewPositionsFile = "overview-positions.md"
 369        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 370
 371        See also: `Overview()` with parameter `details="positions"`.
 372        """
 373
 374        self.overviewOrdersFile = "overview-orders.md"
 375        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 376
 377        See also: `Overview()` with parameter `details="orders"`.
 378        """
 379
 380        self.overviewAnalyticsFile = "overview-analytics.md"
 381        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 382
 383        See also: `Overview()` with parameter `details="analytics"`.
 384        """
 385
 386        self.overviewBondsCalendarFile = "overview-calendar.md"
 387        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 388
 389        See also: `Overview()` with parameter `details="calendar"`.
 390        """
 391
 392        self.reportFile = "deals.md"
 393        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 394
 395        See also: `Deals()`.
 396        """
 397
 398        self.withdrawalLimitsFile = "limits.md"
 399        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 400
 401        See also: `OverviewLimits()` and `RequestLimits()`.
 402        """
 403
 404        self.userInfoFile = "user-info.md"
 405        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 406
 407        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 408        """
 409
 410        self.userAccountsFile = "accounts.md"
 411        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 412
 413        See also: `OverviewAccounts()`, `RequestAccounts()`.
 414        """
 415
 416        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 417        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 418
 419        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 420
 421        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 422        """
 423
 424        self.iList = None  # init iList for raw instruments data
 425        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 426        
 427        See also: `Listing()`, `DumpInstruments()`.
 428        """
 429
 430        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 431        if useCache:
 432            if os.path.exists(self.iListDumpFile):
 433                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 434                curTime = datetime.now(tzutc())
 435
 436                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 437                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 438
 439                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 440
 441                else:
 442                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 443
 444                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 445                        os.path.abspath(self.iListDumpFile),
 446                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 447                    ))
 448
 449            else:
 450                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 451                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 452
 453        else:
 454            self.iList = self.Listing()  # request new raw instruments data from broker server
 455            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 456
 457        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 458        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 459
 460        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 461        """
 462
 463    def _ParseJSON(self, rawData="{}") -> dict:
 464        """
 465        Parse JSON from response string.
 466
 467        :param rawData: this is a string with JSON-formatted text.
 468        :return: JSON (dictionary), parsed from server response string.
 469        """
 470        responseJSON = json.loads(rawData) if rawData else {}
 471
 472        if self.moreDebug:
 473            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 474
 475        return responseJSON
 476
 477    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 478        """
 479        Send GET or POST request to broker server and receive JSON object.
 480
 481        self.header: must be defining with dictionary of headers.
 482        self.body: if define then used as request body. None by default.
 483        self.timeout: global request timeout, 15 seconds by default.
 484        :param url: url with REST request.
 485        :param reqType: send "GET" or "POST" request. "GET" by default.
 486        :param retry: how many times retry after first request if an 5xx server errors occurred.
 487        :param pause: sleep time in seconds between retries.
 488        :return: response JSON (dictionary) from broker.
 489        """
 490        if reqType not in ("GET", "POST"):
 491            uLogger.error("You can define request type: 'GET' or 'POST'!")
 492            raise Exception("Incorrect value")
 493
 494        if self.moreDebug:
 495            uLogger.debug("Request parameters:")
 496            uLogger.debug("    - REST API URL: {}".format(url))
 497            uLogger.debug("    - request type: {}".format(reqType))
 498            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 499            uLogger.debug("    - body:\n{}".format(self.body))
 500
 501        # fast hack to avoid all operations with some tickers/FIGI
 502        responseJSON = {}
 503        oK = True
 504        for item in self.exclude:
 505            if item in url:
 506                if self.moreDebug:
 507                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 508
 509                oK = False
 510                break
 511
 512        if oK:
 513            counter = 0
 514            response = None
 515            errMsg = ""
 516
 517            while not response and counter <= retry:
 518                if reqType == "GET":
 519                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 520
 521                if reqType == "POST":
 522                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 523
 524                if self.moreDebug:
 525                    uLogger.debug("Response:")
 526                    uLogger.debug("    - status code: {}".format(response.status_code))
 527                    uLogger.debug("    - reason: {}".format(response.reason))
 528                    uLogger.debug("    - body length: {}".format(len(response.text)))
 529                    uLogger.debug("    - headers:\n{}".format(response.headers))
 530
 531                # Server returns some headers:
 532                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 533                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 534                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 535                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 536                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 537                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 538                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 539                    sleep(rateLimitWait)
 540
 541                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 542                if 400 <= response.status_code < 500:
 543                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 544                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 545                    counter = retry + 1
 546
 547                if 500 <= response.status_code < 600:
 548                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 549                    uLogger.debug("    - not oK, {}".format(errMsg))
 550                    counter += 1
 551
 552                    if counter <= retry:
 553                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 554                        sleep(pause)
 555
 556            responseJSON = self._ParseJSON(rawData=response.text)
 557
 558            if errMsg:
 559                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 560                uLogger.error("    - not oK, {}".format(errMsg))
 561
 562        return responseJSON
 563
 564    def _IUpdater(self, iType: str) -> tuple:
 565        """
 566        Request instrument by type from server. See available API methods for instruments:
 567        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 568        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 569        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 570        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 571        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 572
 573        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 574        :return: tuple with iType name and list of available instruments of current type for defined user token.
 575        """
 576        result = []
 577
 578        if iType in TKS_INSTRUMENTS:
 579            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 580
 581            # all instruments have the same body in API v2 requests:
 582            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 583            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 584            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 585
 586        return iType, result
 587
 588    def _IWrapper(self, kwargs):
 589        """
 590        Wrapper runs instrument's update method `_IUpdater()`.
 591        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 592        """
 593        return self._IUpdater(**kwargs)
 594
 595    def Listing(self) -> dict:
 596        """
 597        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 598
 599        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 600        """
 601        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 602        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 603
 604        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 605        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 606        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 607
 608        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 609        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 610        poolUpdater.close()
 611
 612        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 613        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 614        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 615
 616        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 617        for iType in iList.keys():
 618            for ticker in iList[iType]:
 619                iList[iType][ticker]["type"] = iType
 620
 621                if "minPriceIncrement" in iList[iType][ticker].keys():
 622                    iList[iType][ticker]["step"] = NanoToFloat(
 623                        iList[iType][ticker]["minPriceIncrement"]["units"],
 624                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 625                    )
 626
 627                else:
 628                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 629
 630        return iList
 631
 632    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 633        """
 634        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 635
 636        See also: `DumpInstruments()`, `Listing()`.
 637
 638        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 639                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 640        """
 641        if self.iListDumpFile is None or not self.iListDumpFile:
 642            uLogger.error("Output name of dump file must be defined!")
 643            raise Exception("Filename required")
 644
 645        if not self.iList or forceUpdate:
 646            self.iList = self.Listing()
 647
 648        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 649
 650        # Save as XLSX with separated sheets for every type of instruments:
 651        with pd.ExcelWriter(
 652                path=xlsxDumpFile,
 653                date_format=TKS_DATE_FORMAT,
 654                datetime_format=TKS_DATE_TIME_FORMAT,
 655                mode="w",
 656        ) as writer:
 657            for iType in TKS_INSTRUMENTS:
 658                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 659                df = df[sorted(df)]  # sorted by column names
 660                df = df.applymap(
 661                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 662                    na_action="ignore",
 663                )  # converting numbers from nano-type to float in every cell
 664                df.to_excel(
 665                    writer,
 666                    sheet_name=iType,
 667                    encoding="UTF-8",
 668                    freeze_panes=(1, 1),
 669                )  # saving as XLSX-file with freeze first row and column as headers
 670
 671        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 672
 673    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 674        """
 675        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 676        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 677
 678        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 679
 680        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 681                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 682        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 683        """
 684        if self.iListDumpFile is None or not self.iListDumpFile:
 685            uLogger.error("Output name of dump file must be defined!")
 686            raise Exception("Filename required")
 687
 688        if not self.iList or forceUpdate:
 689            self.iList = self.Listing()
 690
 691        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 692        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 693            fH.write(jsonDump)
 694
 695        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 696
 697        return jsonDump
 698
 699    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 700        """
 701        Show information about one instrument defined by json data and prints it in Markdown format.
 702
 703        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 704
 705        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 706        :param show: if `True` then also printing information about instrument and its current price.
 707        :return: multilines text in Markdown format with information about one instrument.
 708        """
 709        splitLine = "|                                                             |                                                        |\n"
 710        infoText = ""
 711
 712        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 713            info = [
 714                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 715                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 716                "| Parameters                                                  | Values                                                 |\n",
 717                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 718                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 719                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 720            ]
 721
 722            if "sector" in iJSON.keys() and iJSON["sector"]:
 723                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 724
 725            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 726                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 727                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 728            )))
 729
 730            info.extend([
 731                splitLine,
 732                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 733                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 734            ])
 735
 736            if "isin" in iJSON.keys() and iJSON["isin"]:
 737                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 738
 739            if "classCode" in iJSON.keys():
 740                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 741
 742            info.extend([
 743                splitLine,
 744                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 745                splitLine,
 746                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 747                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 748                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 749            ])
 750
 751            if iJSON["figi"]:
 752                self.figi = iJSON["figi"]
 753                iJSON = iJSON | self.RequestTradingStatus()
 754
 755                info.extend([
 756                    splitLine,
 757                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 758                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 759                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 760                ])
 761
 762            info.append(splitLine)
 763
 764            if "type" in iJSON.keys() and iJSON["type"]:
 765                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 766
 767            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 768                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 769
 770            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 771                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 772
 773            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 774                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 775
 776            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 777                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 778
 779            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 780                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 781
 782            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 783                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 784
 785            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 786                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 787
 788            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 789                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 790
 791            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 792                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 793
 794            if "currency" in iJSON.keys():
 795                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 796
 797            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 798                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 799
 800            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 801                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 804                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 807                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 808
 809            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 810                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 811
 812            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 813                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 814
 815            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 816                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 817
 818            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 819                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 820
 821            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 822                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 823
 824            iExt = None
 825            if iJSON["type"] == "Bonds":
 826                info.extend([
 827                    splitLine,
 828                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 829                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 830                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 831                        iJSON["nominal"]["currency"],
 832                    )),
 833                ])
 834
 835                if "floatingCouponFlag" in iJSON.keys():
 836                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 837
 838                if "amortizationFlag" in iJSON.keys():
 839                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 840
 841                info.append(splitLine)
 842
 843                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 844                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 845
 846                if iJSON["figi"]:
 847                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 848
 849                    info.extend([
 850                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 851                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 852                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 853                    ])
 854
 855                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 856                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 857                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 858                        iJSON["aciValue"]["currency"]
 859                    )))
 860
 861            if "currentPrice" in iJSON.keys():
 862                info.append(splitLine)
 863
 864                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 865                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 866
 867                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 868                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 869                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 870                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 871                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 872
 873                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 874                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 875
 876                info.extend([
 877                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 878                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 879                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 880                    )),
 881                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 882                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 883                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 884                    )),
 885                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 886                        "{:.2f}%{}".format(
 887                            iJSON["currentPrice"]["changes"],
 888                            " ({}{:.2f} {})".format(
 889                                "+" if bondChangesDelta > 0 else "",
 890                                bondChangesDelta,
 891                                aciCurrency
 892                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 893                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 894                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 895                                currency
 896                            ),
 897                        )
 898                    ),
 899                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 900                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 901                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 902                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 903                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 904                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 905                    )),
 906                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 907                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 908                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 909                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 910                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 911                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 912                    )),
 913                ])
 914
 915            if "lot" in iJSON.keys():
 916                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 917
 918            if "step" in iJSON.keys() and iJSON["step"] != 0:
 919                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 920
 921            # Add bond payment calendar:
 922            if iJSON["type"] == "Bonds":
 923                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 924                info.extend(["\n", strCalendar])
 925
 926            infoText += "".join(info)
 927
 928            if show:
 929                uLogger.info("{}".format(infoText))
 930
 931            else:
 932                uLogger.debug("{}".format(infoText))
 933
 934            if self.infoFile is not None:
 935                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 936                    fH.write(infoText)
 937
 938                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 939
 940        return infoText
 941
 942    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 943        """
 944        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 945
 946        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 947        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 948        :return: JSON formatted data with information about instrument.
 949        """
 950        tickerJSON = {}
 951        if self.moreDebug:
 952            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 953
 954        if not self.ticker:
 955            uLogger.warning("self.ticker variable is not be empty!")
 956
 957        else:
 958            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 959                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 960                raise Exception("Instrument not allowed")
 961
 962            if not self.iList:
 963                self.iList = self.Listing()
 964
 965            if self.ticker in self.iList["Shares"].keys():
 966                tickerJSON = self.iList["Shares"][self.ticker]
 967                if self.moreDebug:
 968                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Currencies"].keys():
 971                tickerJSON = self.iList["Currencies"][self.ticker]
 972                if self.moreDebug:
 973                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Bonds"].keys():
 976                tickerJSON = self.iList["Bonds"][self.ticker]
 977                if self.moreDebug:
 978                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 979
 980            elif self.ticker in self.iList["Etfs"].keys():
 981                tickerJSON = self.iList["Etfs"][self.ticker]
 982                if self.moreDebug:
 983                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 984
 985            elif self.ticker in self.iList["Futures"].keys():
 986                tickerJSON = self.iList["Futures"][self.ticker]
 987                if self.moreDebug:
 988                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 989
 990        if tickerJSON:
 991            self.figi = tickerJSON["figi"]
 992
 993            if requestPrice:
 994                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 995
 996                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 997                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 998
 999                else:
1000                    tickerJSON["currentPrice"]["changes"] = 0
1001
1002            if show:
1003                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1004
1005        else:
1006            if show:
1007                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1008
1009        return tickerJSON
1010
1011    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1012        """
1013        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1014
1015        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1016        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1017        :return: JSON formatted data with information about instrument.
1018        """
1019        figiJSON = {}
1020        if self.moreDebug:
1021            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1022
1023        if not self.figi:
1024            uLogger.warning("self.figi variable is not be empty!")
1025
1026        else:
1027            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1028                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1029                raise Exception("Instrument not allowed")
1030
1031            if not self.iList:
1032                self.iList = self.Listing()
1033
1034            for item in self.iList["Shares"].keys():
1035                if self.figi == self.iList["Shares"][item]["figi"]:
1036                    figiJSON = self.iList["Shares"][item]
1037
1038                    if self.moreDebug:
1039                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1040
1041                    break
1042
1043            if not figiJSON:
1044                for item in self.iList["Currencies"].keys():
1045                    if self.figi == self.iList["Currencies"][item]["figi"]:
1046                        figiJSON = self.iList["Currencies"][item]
1047
1048                        if self.moreDebug:
1049                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1050
1051                        break
1052
1053            if not figiJSON:
1054                for item in self.iList["Bonds"].keys():
1055                    if self.figi == self.iList["Bonds"][item]["figi"]:
1056                        figiJSON = self.iList["Bonds"][item]
1057
1058                        if self.moreDebug:
1059                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1060
1061                        break
1062
1063            if not figiJSON:
1064                for item in self.iList["Etfs"].keys():
1065                    if self.figi == self.iList["Etfs"][item]["figi"]:
1066                        figiJSON = self.iList["Etfs"][item]
1067
1068                        if self.moreDebug:
1069                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1070
1071                        break
1072
1073            if not figiJSON:
1074                for item in self.iList["Futures"].keys():
1075                    if self.figi == self.iList["Futures"][item]["figi"]:
1076                        figiJSON = self.iList["Futures"][item]
1077
1078                        if self.moreDebug:
1079                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1080
1081                        break
1082
1083        if figiJSON:
1084            self.figi = figiJSON["figi"]
1085            self.ticker = figiJSON["ticker"]
1086
1087            if requestPrice:
1088                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1089
1090                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1091                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1092
1093                else:
1094                    figiJSON["currentPrice"]["changes"] = 0
1095
1096            if show:
1097                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1098
1099        else:
1100            if show:
1101                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1102
1103        return figiJSON
1104
1105    def GetCurrentPrices(self, show: bool = True) -> dict:
1106        """
1107        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1108        `{"buy": [{"price": 1243.8, "quantity": 193},
1109                  {"price": 1244.0, "quantity": 168},
1110                  {"price": 1244.8, "quantity": 5},
1111                  {"price": 1245.0, "quantity": 61},
1112                  {"price": 1245.4, "quantity": 60}],
1113          "sell": [{"price": 1243.6, "quantity": 8},
1114                   {"price": 1242.6, "quantity": 10},
1115                   {"price": 1242.4, "quantity": 18},
1116                   {"price": 1242.2, "quantity": 50},
1117                   {"price": 1242.0, "quantity": 113}],
1118          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1119        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1120        - sell: list of dicts with Buyers prices,
1121            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1122            - quantity: volume value by current price in lots,
1123        - limitUp: current trade session limit price, maximum,
1124        - limitDown: current trade session limit price, minimum,
1125        - lastPrice: last deal price of the instrument,
1126        - closePrice: previous trade session close price of the instrument.
1127
1128        See also: `SearchByTicker()` and `SearchByFIGI()`.
1129        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1130        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1131
1132        :param show: if `True` then print DOM to log and console.
1133        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1134                 If an error occurred then returns an empty record:
1135                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1136        """
1137        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1138
1139        if self.depth < 1:
1140            uLogger.error("Depth of Market (DOM) must be >=1!")
1141            raise Exception("Incorrect value")
1142
1143        if not (self.ticker or self.figi):
1144            uLogger.error("self.ticker or self.figi variables must be defined!")
1145            raise Exception("Ticker or FIGI required")
1146
1147        if self.ticker and not self.figi:
1148            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1149            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1150
1151        if not self.ticker and self.figi:
1152            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1153            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1154
1155        if not self.figi:
1156            uLogger.error("FIGI is not defined!")
1157            raise Exception("Ticker or FIGI required")
1158
1159        else:
1160            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1161
1162            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1163            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1164            self.body = str({"figi": self.figi, "depth": self.depth})
1165            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1166
1167            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1168                # list of dicts with sellers orders:
1169                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1170
1171                # list of dicts with buyers orders:
1172                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1173
1174                # max price of instrument at this time:
1175                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1176
1177                # min price of instrument at this time:
1178                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1179
1180                # last price of deal with instrument:
1181                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1182
1183                # last close price of instrument:
1184                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1185
1186            else:
1187                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1188                uLogger.debug("Server response: {}".format(pricesResponse))
1189
1190            if show:
1191                if prices["buy"] or prices["sell"]:
1192                    info = [
1193                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1194                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1195                            self.ticker,
1196                            self.figi,
1197                            self.depth,
1198                        ),
1199                        "-" * 60, "\n",
1200                        "             Orders of Buyers | Orders of Sellers\n",
1201                        "-" * 60, "\n",
1202                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1203                        "-" * 60, "\n",
1204                    ]
1205
1206                    if not prices["buy"]:
1207                        info.append("                              | No orders!\n")
1208                        sumBuy = 0
1209
1210                    else:
1211                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1212                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1213                        for item in maxMinSorted:
1214                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1215
1216                    if not prices["sell"]:
1217                        info.append("No orders!                    |\n")
1218                        sumSell = 0
1219
1220                    else:
1221                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1222                        for item in prices["sell"]:
1223                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1224
1225                    info.extend([
1226                        "-" * 60, "\n",
1227                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1228                        "-" * 60, "\n",
1229                    ])
1230
1231                    infoText = "".join(info)
1232
1233                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1234
1235                else:
1236                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1237
1238        return prices
1239
1240    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1241        """
1242        This method get and show information about all available broker instruments for current user account.
1243        If `instrumentsFile` string is not empty then also save information to this file.
1244
1245        :param show: if `True` then print results to console, if `False` — print only to file.
1246        :return: multi-lines string with all available broker instruments
1247        """
1248        if not self.iList:
1249            self.iList = self.Listing()
1250
1251        info = [
1252            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1253            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1254        ]
1255
1256        # add instruments count by type:
1257        for iType in self.iList.keys():
1258            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1259
1260        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1261        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1262
1263        # generating info tables with all instruments by type:
1264        for iType in self.iList.keys():
1265            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1266
1267            for instrument in self.iList[iType].keys():
1268                iName = self.iList[iType][instrument]["name"]  # instrument's name
1269                if len(iName) > 57:
1270                    iName = "{}...".format(iName[:54])  # right trim for a long string
1271
1272                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1273                    self.iList[iType][instrument]["ticker"],
1274                    iName,
1275                    self.iList[iType][instrument]["figi"],
1276                    self.iList[iType][instrument]["currency"],
1277                    self.iList[iType][instrument]["lot"],
1278                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1279                ))
1280
1281        infoText = "".join(info)
1282
1283        if show:
1284            uLogger.info(infoText)
1285
1286        if self.instrumentsFile:
1287            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1288                fH.write(infoText)
1289
1290            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1291
1292        return infoText
1293
1294    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1295        """
1296        This method search and show information about instruments by part of its ticker, FIGI or name.
1297        If `searchResultsFile` string is not empty then also save information to this file.
1298
1299        :param pattern: string with part of ticker, FIGI or instrument's name.
1300        :param show: if `True` then print results to console, if `False` — return list of result only.
1301        :return: list of dictionaries with all found instruments.
1302        """
1303        if not self.iList:
1304            self.iList = self.Listing()
1305
1306        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1307        compiledPattern = re.compile(pattern, re.IGNORECASE)
1308
1309        for iType in self.iList:
1310            for instrument in self.iList[iType].values():
1311                searchResult = compiledPattern.search(" ".join(
1312                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1313                ))
1314
1315                if searchResult:
1316                    searchResults[iType][instrument["ticker"]] = instrument
1317
1318        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1319        info = [
1320            "# Search results\n\n",
1321            "* **Search pattern:** [{}]\n".format(pattern),
1322            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1323            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1324        ]
1325        infoShort = info[:]
1326
1327        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1328        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1329        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1330
1331        if resultsLen == 0:
1332            info.append("\nNo results\n")
1333            infoShort.append("\nNo results\n")
1334            uLogger.warning("No results. Try changing your search pattern.")
1335
1336        else:
1337            for iType in searchResults:
1338                iTypeValuesCount = len(searchResults[iType].values())
1339                if iTypeValuesCount > 0:
1340                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1341                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342
1343                    for instrument in searchResults[iType].values():
1344                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1345                            instrument["type"],
1346                            instrument["ticker"],
1347                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1348                            instrument["figi"],
1349                        ))
1350
1351                    if iTypeValuesCount <= 5:
1352                        infoShort.extend(info[-iTypeValuesCount:])
1353
1354                    else:
1355                        infoShort.extend(info[-5:])
1356                        infoShort.append(skippedLine)
1357
1358        infoText = "".join(info)
1359        infoTextShort = "".join(infoShort)
1360
1361        if show:
1362            uLogger.info(infoTextShort)
1363            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1364
1365        if self.searchResultsFile:
1366            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1367                fH.write(infoText)
1368
1369            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1370
1371        return searchResults
1372
1373    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1374        """
1375        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1376
1377        :param instruments: list of strings with tickers or FIGIs.
1378        :return: list with unique instrument FIGIs only.
1379        """
1380        requestedInstruments = []
1381        for iName in instruments:
1382            if iName not in self.aliases.keys():
1383                if iName not in requestedInstruments:
1384                    requestedInstruments.append(iName)
1385
1386            else:
1387                if iName not in requestedInstruments:
1388                    if self.aliases[iName] not in requestedInstruments:
1389                        requestedInstruments.append(self.aliases[iName])
1390
1391        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1392
1393        onlyUniqueFIGIs = []
1394        for iName in requestedInstruments:
1395            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1396                continue
1397
1398            self.ticker = iName
1399            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1400
1401            if not iData:
1402                self.ticker = ""
1403                self.figi = iName
1404
1405                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1406
1407                if not iData:
1408                    self.figi = ""
1409                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1410
1411            if iData and iData["figi"] not in onlyUniqueFIGIs:
1412                onlyUniqueFIGIs.append(iData["figi"])
1413
1414        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1415
1416        return onlyUniqueFIGIs
1417
1418    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1419        """
1420        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1421
1422        See limits: https://tinkoff.github.io/investAPI/limits/
1423
1424        If `pricesFile` string is not empty then also save information to this file.
1425
1426        :param instruments: list of strings with tickers or FIGIs.
1427        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1428        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1429                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1430        """
1431        if instruments is None or not instruments:
1432            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1433            raise Exception("Ticker or FIGI required")
1434
1435        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1436
1437        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1438
1439        iList = []  # trying to get info and current prices about all unique instruments:
1440        for self.figi in onlyUniqueFIGIs:
1441            iData = self.SearchByFIGI(requestPrice=True)
1442            iList.append(iData)
1443
1444        self.ShowListOfPrices(iList, show)
1445
1446        return iList
1447
1448    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1449        """
1450        Show table contains current prices of given instruments.
1451
1452        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1453                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1454        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1455        :return: multilines text in Markdown format as a table contains current prices.
1456        """
1457        infoText = ""
1458
1459        if show or self.pricesFile:
1460            info = [
1461                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1462                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1463                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1464            ]
1465
1466            for item in iList:
1467                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1468                    item["ticker"],
1469                    item["figi"],
1470                    item["type"],
1471                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1472                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1473                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1474                    "{} / {}".format(
1475                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1476                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1477                    ),
1478                    "{} / {}".format(
1479                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1480                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1481                    ),
1482                    item["currency"],
1483                ))
1484
1485            infoText = "".join(info)
1486
1487            if show:
1488                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1489
1490            if self.pricesFile:
1491                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1492                    fH.write(infoText)
1493
1494                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1495
1496        return infoText
1497
1498    def RequestTradingStatus(self) -> dict:
1499        """
1500        Requesting trading status for the instrument defined by `figi` variable.
1501
1502        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1503
1504        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1505
1506        :return: dictionary with trading status attributes. Response example:
1507                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1508                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1509        """
1510        if self.figi is None or not self.figi:
1511            uLogger.error("Variable `figi` must be defined for using this method!")
1512            raise Exception("FIGI required")
1513
1514        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1515
1516        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1517        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1518        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1519
1520        if self.moreDebug:
1521            uLogger.debug("Records about current trading status successfully received")
1522
1523        return tradingStatus
1524
1525    def RequestPortfolio(self) -> dict:
1526        """
1527        Requesting actual user's portfolio for current `accountId`.
1528
1529        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1530
1531        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1532
1533        :return: dictionary with user's portfolio.
1534        """
1535        if self.accountId is None or not self.accountId:
1536            uLogger.error("Variable `accountId` must be defined for using this method!")
1537            raise Exception("Account ID required")
1538
1539        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1540
1541        self.body = str({"accountId": self.accountId})
1542        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1543        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1544
1545        if self.moreDebug:
1546            uLogger.debug("Records about user's portfolio successfully received")
1547
1548        return rawPortfolio
1549
1550    def RequestPositions(self) -> dict:
1551        """
1552        Requesting open positions by currencies and instruments for current `accountId`.
1553
1554        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1555
1556        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1557
1558        :return: dictionary with open positions by instruments.
1559        """
1560        if self.accountId is None or not self.accountId:
1561            uLogger.error("Variable `accountId` must be defined for using this method!")
1562            raise Exception("Account ID required")
1563
1564        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1565
1566        self.body = str({"accountId": self.accountId})
1567        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1568        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1569
1570        if self.moreDebug:
1571            uLogger.debug("Records about current open positions successfully received")
1572
1573        return rawPositions
1574
1575    def RequestPendingOrders(self) -> list:
1576        """
1577        Requesting current actual pending orders for current `accountId`.
1578
1579        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1580
1581        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1582
1583        :return: list of dictionaries with pending orders.
1584        """
1585        if self.accountId is None or not self.accountId:
1586            uLogger.error("Variable `accountId` must be defined for using this method!")
1587            raise Exception("Account ID required")
1588
1589        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1590
1591        self.body = str({"accountId": self.accountId})
1592        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1593        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1594
1595        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1596
1597        return rawOrders
1598
1599    def RequestStopOrders(self) -> list:
1600        """
1601        Requesting current actual stop orders for current `accountId`.
1602
1603        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1604
1605        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1606
1607        :return: list of dictionaries with stop orders.
1608        """
1609        if self.accountId is None or not self.accountId:
1610            uLogger.error("Variable `accountId` must be defined for using this method!")
1611            raise Exception("Account ID required")
1612
1613        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1614
1615        self.body = str({"accountId": self.accountId})
1616        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1617        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1618
1619        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1620
1621        return rawStopOrders
1622
1623    def Overview(self, show: bool = False, details: str = "full") -> dict:
1624        """
1625        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1626        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1627        and `overviewBondsCalendarFile` are defined then also save information to file.
1628
1629        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1630        many requests about the state of the portfolio, and then, based on the received data, a large number
1631        of calculation and statistics are collected.
1632
1633        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1634        :param details: how detailed should the information be?
1635        - `full` — shows full available information about portfolio status (by default),
1636        - `positions` — shows only open positions,
1637        - `orders` — shows only sections of open limits and stop orders.
1638        - `digest` — show a short digest of the portfolio status,
1639        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1640        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1641        :return: dictionary with client's raw portfolio and some statistics.
1642        """
1643        if self.accountId is None or not self.accountId:
1644            uLogger.error("Variable `accountId` must be defined for using this method!")
1645            raise Exception("Account ID required")
1646
1647        view = {
1648            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1649                "headers": {},  # list of dictionaries, response headers without "positions" section
1650                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1651                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1652                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1653                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1654                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1655                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1656                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1657                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1658                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1659            },
1660            "stat": {  # --- some statistics calculated using "raw" sections:
1661                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1662                "availableRUB": 0.,  # available rubles (without other currencies)
1663                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1664                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1665                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1666                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1667                "sharesCostRUB": 0.,  # costs of all shares in RUB
1668                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1669                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1670                "futuresCostRUB": 0.,  # costs of all futures in RUB
1671                "Currencies": [],  # list of dictionaries of all currencies statistics
1672                "Shares": [],  # list of dictionaries of all shares statistics
1673                "Bonds": [],  # list of dictionaries of all bonds statistics
1674                "Etfs": [],  # list of dictionaries of all etfs statistics
1675                "Futures": [],  # list of dictionaries of all futures statistics
1676                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1677                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1678                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1679                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1680                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1681            },
1682            "analytics": {  # --- some analytics of portfolio:
1683                "distrByAssets": {},  # portfolio distribution by assets
1684                "distrByCompanies": {},  # portfolio distribution by companies
1685                "distrBySectors": {},  # portfolio distribution by sectors
1686                "distrByCurrencies": {},  # portfolio distribution by currencies
1687                "distrByCountries": {},  # portfolio distribution by countries
1688                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1689            }
1690        }
1691
1692        details = details.lower()
1693        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1694        if details not in availableDetails:
1695            details = "full"
1696            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1697
1698        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1699
1700        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1701        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1702        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1703        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1704
1705        # save response headers without "positions" section:
1706        for key in portfolioResponse.keys():
1707            if key != "positions":
1708                view["raw"]["headers"][key] = portfolioResponse[key]
1709
1710            else:
1711                continue
1712
1713        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1714        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1715        for item in portfolioResponse["positions"]:
1716            if item["instrumentType"] == "currency":
1717                self.figi = item["figi"]
1718                curr = self.SearchByFIGI(requestPrice=False)
1719
1720                # current price of currency in RUB:
1721                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1722                    "name": curr["name"],
1723                    "currentPrice": NanoToFloat(
1724                        item["currentPrice"]["units"],
1725                        item["currentPrice"]["nano"]
1726                    ),
1727                }
1728
1729                view["raw"]["Currencies"].append(item)
1730
1731            elif item["instrumentType"] == "share":
1732                view["raw"]["Shares"].append(item)
1733
1734            elif item["instrumentType"] == "bond":
1735                view["raw"]["Bonds"].append(item)
1736
1737            elif item["instrumentType"] == "etf":
1738                view["raw"]["Etfs"].append(item)
1739
1740            elif item["instrumentType"] == "futures":
1741                view["raw"]["Futures"].append(item)
1742
1743            else:
1744                continue
1745
1746        # how many volume of currencies (by ISO currency name) are blocked:
1747        for item in view["raw"]["positions"]["blocked"]:
1748            blocked = NanoToFloat(item["units"], item["nano"])
1749            if blocked > 0:
1750                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1751
1752        # how many volume of instruments (by FIGI) are blocked:
1753        for item in view["raw"]["positions"]["securities"]:
1754            blocked = int(item["blocked"])
1755            if blocked > 0:
1756                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1757
1758        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1759
1760        if "rub" in allBlocked.keys():
1761            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1762
1763        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1764        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1765        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1766        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1767        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1768        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1769        view["stat"]["portfolioCostRUB"] = sum([
1770            view["stat"]["allCurrenciesCostRUB"],
1771            view["stat"]["sharesCostRUB"],
1772            view["stat"]["bondsCostRUB"],
1773            view["stat"]["etfsCostRUB"],
1774            view["stat"]["futuresCostRUB"],
1775        ])
1776
1777        # --- calculating some portfolio statistics:
1778        byComp = {}  # distribution by companies
1779        bySect = {}  # distribution by sectors
1780        byCurr = {}  # distribution by currencies (include RUB)
1781        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1782        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1783
1784        for item in portfolioResponse["positions"]:
1785            self.figi = item["figi"]
1786            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1787
1788            if instrument:
1789                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1790                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1791
1792                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1793                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1794
1795                else:
1796                    blocked = 0
1797
1798                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1799                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1800                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1801                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1802                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1803                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1804                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1805                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1806                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1807                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1808                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1809                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1810
1811                statData = {
1812                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1813                    "ticker": instrument["ticker"],  # ticker by FIGI
1814                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1815                    "volume": volume,  # available volume of instrument
1816                    "lots": lots,  # volume in lots of instrument
1817                    "direction": direction,  # direction of an instrument's position: short or long
1818                    "blocked": blocked,  # blocked volume of currency or instrument
1819                    "currentPrice": curPrice,  # current instrument's price in basic asset
1820                    "average": average,  # current average position price
1821                    "cost": cost,  # current cost of all volume of instrument in basic asset
1822                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1823                    "costRUB": costRUB,  # cost of instrument in ruble
1824                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1825                    "profit": profit,  # expected profit at current moment
1826                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1827                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1828                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1829                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1830                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1831                    "step": instrument["step"],  # minimum price increment
1832                }
1833
1834                # adding distribution by unique countries:
1835                if statData["country"] not in byCountry.keys():
1836                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1837
1838                else:
1839                    byCountry[statData["country"]]["cost"] += costRUB
1840                    byCountry[statData["country"]]["percent"] += percentCostRUB
1841
1842                if item["instrumentType"] != "currency":
1843                    # adding distribution by unique companies:
1844                    if statData["name"]:
1845                        if statData["name"] not in byComp.keys():
1846                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1847
1848                        else:
1849                            byComp[statData["name"]]["cost"] += costRUB
1850                            byComp[statData["name"]]["percent"] += percentCostRUB
1851
1852                    # adding distribution by unique sectors:
1853                    if statData["sector"] not in bySect.keys():
1854                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1855
1856                    else:
1857                        bySect[statData["sector"]]["cost"] += costRUB
1858                        bySect[statData["sector"]]["percent"] += percentCostRUB
1859
1860                # adding distribution by unique currencies:
1861                if currency not in byCurr.keys():
1862                    byCurr[currency] = {
1863                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1864                        "cost": costRUB,
1865                        "percent": percentCostRUB
1866                    }
1867
1868                else:
1869                    byCurr[currency]["cost"] += costRUB
1870                    byCurr[currency]["percent"] += percentCostRUB
1871
1872                # saving statistics for every instrument:
1873                if item["instrumentType"] == "currency":
1874                    view["stat"]["Currencies"].append(statData)
1875
1876                    # update dict with free funds for trading (total - blocked) by currencies
1877                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1878                    view["stat"]["funds"][currency] = {
1879                        "total": volume,
1880                        "totalCostRUB": costRUB,  # total volume cost in rubles
1881                        "free": volume - blocked,
1882                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1883                    }
1884
1885                elif item["instrumentType"] == "share":
1886                    view["stat"]["Shares"].append(statData)
1887
1888                elif item["instrumentType"] == "bond":
1889                    view["stat"]["Bonds"].append(statData)
1890
1891                elif item["instrumentType"] == "etf":
1892                    view["stat"]["Etfs"].append(statData)
1893
1894                elif item["instrumentType"] == "Futures":
1895                    view["stat"]["Futures"].append(statData)
1896
1897                else:
1898                    continue
1899
1900        # total changes in Russian Ruble:
1901        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1902        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1903        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1904        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1905        view["stat"]["funds"]["rub"] = {
1906            "total": view["stat"]["availableRUB"],
1907            "totalCostRUB": view["stat"]["availableRUB"],
1908            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1909            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910        }
1911
1912        # --- pending orders sector data:
1913        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1914        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1915
1916        for item in view["raw"]["orders"]:
1917            self.figi = item["figi"]
1918
1919            if item["figi"] not in uniquePendingOrdersFIGIs:
1920                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1921
1922                uniquePendingOrdersFIGIs.append(item["figi"])
1923                uniquePendingOrders[item["figi"]] = instrument
1924
1925            else:
1926                instrument = uniquePendingOrders[item["figi"]]
1927
1928            if instrument:
1929                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1930                orderType = TKS_ORDER_TYPES[item["orderType"]]
1931                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1932                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1933
1934                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1935                if item["direction"] == "ORDER_DIRECTION_BUY":
1936                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1937
1938                else:
1939                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1940
1941                # requested price for order execution:
1942                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1943
1944                # necessary changes in percent to reach target from current price:
1945                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1946
1947                view["stat"]["orders"].append({
1948                    "orderID": item["orderId"],  # orderId number parameter of current order
1949                    "figi": item["figi"],  # FIGI identification
1950                    "ticker": instrument["ticker"],  # ticker name by FIGI
1951                    "lotsRequested": item["lotsRequested"],  # requested lots value
1952                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1953                    "currentPrice": lastPrice,  # current instrument's price for defined action
1954                    "targetPrice": target,  # requested price for order execution in base currency
1955                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1956                    "percentChanges": changes,  # changes in percent to target from current price
1957                    "currency": item["currency"],  # instrument's currency name
1958                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1959                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1960                    "status": orderState,  # order status from TKS_ORDER_STATES
1961                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1962                })
1963
1964        # --- stop orders sector data:
1965        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1966        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1967
1968        for item in view["raw"]["stopOrders"]:
1969            self.figi = item["figi"]
1970
1971            if item["figi"] not in uniqueStopOrdersFIGIs:
1972                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1973
1974                uniqueStopOrdersFIGIs.append(item["figi"])
1975                uniqueStopOrders[item["figi"]] = instrument
1976
1977            else:
1978                instrument = uniqueStopOrders[item["figi"]]
1979
1980            if instrument:
1981                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1982                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1983                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1984
1985                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1986                if "expirationTime" in item.keys():
1987                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1988                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1989
1990                else:
1991                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1992                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1993
1994                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1995                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1996                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1997
1998                else:
1999                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2000
2001                # requested price when stop-order executed:
2002                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2003
2004                # price for limit-order, set up when stop-order executed:
2005                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2006
2007                # necessary changes in percent to reach target from current price:
2008                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2009
2010                view["stat"]["stopOrders"].append({
2011                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2012                    "figi": item["figi"],  # FIGI identification
2013                    "ticker": instrument["ticker"],  # ticker name by FIGI
2014                    "lotsRequested": item["lotsRequested"],  # requested lots value
2015                    "currentPrice": lastPrice,  # current instrument's price for defined action
2016                    "targetPrice": target,  # requested price for stop-order execution in base currency
2017                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2018                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2019                    "percentChanges": changes,  # changes in percent to target from current price
2020                    "currency": item["currency"],  # instrument's currency name
2021                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2022                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2023                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2024                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2025                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2026                })
2027
2028        # --- calculating data for analytics section:
2029        # portfolio distribution by assets:
2030        view["analytics"]["distrByAssets"] = {
2031            "Ruble": {
2032                "uniques": 1,
2033                "cost": view["stat"]["availableRUB"],
2034                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2035            },
2036            "Currencies": {
2037                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2038                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2039                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2040            },
2041            "Shares": {
2042                "uniques": len(view["stat"]["Shares"]),
2043                "cost": view["stat"]["sharesCostRUB"],
2044                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2045            },
2046            "Bonds": {
2047                "uniques": len(view["stat"]["Bonds"]),
2048                "cost": view["stat"]["bondsCostRUB"],
2049                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2050            },
2051            "Etfs": {
2052                "uniques": len(view["stat"]["Etfs"]),
2053                "cost": view["stat"]["etfsCostRUB"],
2054                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2055            },
2056            "Futures": {
2057                "uniques": len(view["stat"]["Futures"]),
2058                "cost": view["stat"]["futuresCostRUB"],
2059                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2060            },
2061        }
2062
2063        # portfolio distribution by companies:
2064        view["analytics"]["distrByCompanies"]["All money cash"] = {
2065            "ticker": "",
2066            "cost": view["stat"]["allCurrenciesCostRUB"],
2067            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2068        }
2069        view["analytics"]["distrByCompanies"].update(byComp)
2070
2071        # portfolio distribution by sectors:
2072        view["analytics"]["distrBySectors"]["All money cash"] = {
2073            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2074            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2075        }
2076        view["analytics"]["distrBySectors"].update(bySect)
2077
2078        # portfolio distribution by currencies:
2079        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2080            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2081
2082            if self.moreDebug:
2083                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2084
2085        view["analytics"]["distrByCurrencies"].update(byCurr)
2086        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2087        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2088
2089        # portfolio distribution by countries:
2090        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2091            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2092
2093            if self.moreDebug:
2094                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2095
2096        view["analytics"]["distrByCountries"].update(byCountry)
2097        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2099
2100        # --- Prepare text statistics overview in human-readable:
2101        if show:
2102            # Whatever the value `details`, header not changes:
2103            info = [
2104                "# Client's portfolio\n\n",
2105                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2106                "* **Account ID:** [{}]\n".format(self.accountId),
2107            ]
2108
2109            if details in ["full", "positions", "digest"]:
2110                info.extend([
2111                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2112                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2113                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2114                        view["stat"]["totalChangesRUB"],
2115                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2116                        view["stat"]["totalChangesPercentRUB"],
2117                    ),
2118                ])
2119
2120            if details in ["full", "positions"]:
2121                info.extend([
2122                    "## Open positions\n\n",
2123                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2124                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2125                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2126                        "{:.2f} ({:.2f}) rub".format(
2127                            view["stat"]["availableRUB"],
2128                            view["stat"]["blockedRUB"],
2129                        )
2130                    )
2131                ])
2132
2133                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2134                    return [
2135                        "|                             |                                 |          |              |              |                     |                              |\n",
2136                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2137                            noTradeStr if noTradeStr else typeStr,
2138                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2139                        ),
2140                    ]
2141
2142                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2143                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2144                        "{} [{}]".format(data["ticker"], data["figi"]),
2145                        "{:.2f} ({:.2f}) {}".format(
2146                            data["volume"],
2147                            data["blocked"],
2148                            data["currency"],
2149                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2150                            data["volume"],
2151                            data["blocked"],
2152                        ),
2153                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2154                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2155                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2156                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2157                        "{}{:.2f} {} ({}{:.2f}%)".format(
2158                            "+" if data["profit"] > 0 else "",
2159                            data["profit"], data["baseCurrencyName"],
2160                            "+" if data["percentProfit"] > 0 else "",
2161                            data["percentProfit"],
2162                        ),
2163                    )
2164
2165                # --- Show currencies section:
2166                if view["stat"]["Currencies"]:
2167                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2168                    for item in view["stat"]["Currencies"]:
2169                        info.append(_InfoStr(item, showCurrencyName=True))
2170
2171                else:
2172                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2173
2174                # --- Show shares section:
2175                if view["stat"]["Shares"]:
2176                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2177
2178                    for item in view["stat"]["Shares"]:
2179                        info.append(_InfoStr(item))
2180
2181                else:
2182                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2183
2184                # --- Show bonds section:
2185                if view["stat"]["Bonds"]:
2186                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2187
2188                    for item in view["stat"]["Bonds"]:
2189                        info.append(_InfoStr(item))
2190
2191                else:
2192                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2193
2194                # --- Show etfs section:
2195                if view["stat"]["Etfs"]:
2196                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2197
2198                    for item in view["stat"]["Etfs"]:
2199                        info.append(_InfoStr(item))
2200
2201                else:
2202                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2203
2204                # --- Show futures section:
2205                if view["stat"]["Futures"]:
2206                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2207
2208                    for item in view["stat"]["Futures"]:
2209                        info.append(_InfoStr(item))
2210
2211                else:
2212                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2213
2214            if details in ["full", "orders"]:
2215                # --- Show pending orders section:
2216                if view["stat"]["orders"]:
2217                    info.extend([
2218                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2219                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2220                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2221                    ])
2222
2223                    for item in view["stat"]["orders"]:
2224                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2225                            "{} [{}]".format(item["ticker"], item["figi"]),
2226                            item["orderID"],
2227                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2228                            "{} {} ({}{:.2f}%)".format(
2229                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2230                                item["baseCurrencyName"],
2231                                "+" if item["percentChanges"] > 0 else "",
2232                                float(item["percentChanges"]),
2233                            ),
2234                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2235                            item["action"],
2236                            item["type"],
2237                            item["date"],
2238                        ))
2239
2240                else:
2241                    info.append("\n## Total pending limit-orders: 0\n")
2242
2243                # --- Show stop orders section:
2244                if view["stat"]["stopOrders"]:
2245                    info.extend([
2246                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2247                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2248                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2249                    ])
2250
2251                    for item in view["stat"]["stopOrders"]:
2252                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2253                            "{} [{}]".format(item["ticker"], item["figi"]),
2254                            item["orderID"],
2255                            item["lotsRequested"],
2256                            "{} {} ({}{:.2f}%)".format(
2257                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2258                                item["baseCurrencyName"],
2259                                "+" if item["percentChanges"] > 0 else "",
2260                                float(item["percentChanges"]),
2261                            ),
2262                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2263                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2264                            item["action"],
2265                            item["type"],
2266                            item["expType"],
2267                            item["createDate"],
2268                            item["expDate"],
2269                        ))
2270
2271                else:
2272                    info.append("\n## Total stop-orders: 0\n")
2273
2274            if details in ["full", "analytics"]:
2275                # -- Show analytics section:
2276                if view["stat"]["portfolioCostRUB"] > 0:
2277                    info.extend([
2278                        "\n# Analytics\n"
2279                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2280                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2281                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2282                            view["stat"]["totalChangesRUB"],
2283                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2284                            view["stat"]["totalChangesPercentRUB"],
2285                        ),
2286                        "\n## Portfolio distribution by assets\n"
2287                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2288                        "|------------------------------------|---------|---------|--------------------|\n",
2289                    ])
2290
2291                    for key in view["analytics"]["distrByAssets"].keys():
2292                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2293                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2294                                key,
2295                                view["analytics"]["distrByAssets"][key]["uniques"],
2296                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2297                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2298                            ))
2299
2300                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2301
2302                    info.extend([
2303                        "\n## Portfolio distribution by companies\n"
2304                        "\n| Company                                      | Percent | Current cost       |\n",
2305                        aSepLine,
2306                    ])
2307
2308                    for company in view["analytics"]["distrByCompanies"].keys():
2309                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2310                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2311                                "{}{}".format(
2312                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2313                                    company,
2314                                ),
2315                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2316                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2317                            ))
2318
2319                    info.extend([
2320                        "\n## Portfolio distribution by sectors\n"
2321                        "\n| Sector                                       | Percent | Current cost       |\n",
2322                        aSepLine,
2323                    ])
2324
2325                    for sector in view["analytics"]["distrBySectors"].keys():
2326                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2327                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2328                                sector,
2329                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2330                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2331                            ))
2332
2333                    info.extend([
2334                        "\n## Portfolio distribution by currencies\n"
2335                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2336                        aSepLine,
2337                    ])
2338
2339                    for curr in view["analytics"]["distrByCurrencies"].keys():
2340                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2341                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2342                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2343                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2344                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2345                            ))
2346
2347                    info.extend([
2348                        "\n## Portfolio distribution by countries\n"
2349                        "\n| Assets by country                            | Percent | Current cost       |\n",
2350                        aSepLine,
2351                    ])
2352
2353                    for country in view["analytics"]["distrByCountries"].keys():
2354                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2355                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2356                                country,
2357                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2358                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2359                            ))
2360
2361            if details in ["full", "calendar"]:
2362                # -- Show bonds payment calendar section:
2363                if view["stat"]["Bonds"]:
2364                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2365                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2366                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2367
2368                else:
2369                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2370
2371            infoText = "".join(info)
2372
2373            uLogger.info(infoText)
2374
2375            if details == "full" and self.overviewFile:
2376                filename = self.overviewFile
2377
2378            elif details == "digest" and self.overviewDigestFile:
2379                filename = self.overviewDigestFile
2380
2381            elif details == "positions" and self.overviewPositionsFile:
2382                filename = self.overviewPositionsFile
2383
2384            elif details == "orders" and self.overviewOrdersFile:
2385                filename = self.overviewOrdersFile
2386
2387            elif details == "analytics" and self.overviewAnalyticsFile:
2388                filename = self.overviewAnalyticsFile
2389
2390            elif details == "calendar" and self.overviewBondsCalendarFile:
2391                filename = self.overviewBondsCalendarFile
2392
2393            else:
2394                filename = ""
2395
2396            if filename:
2397                with open(filename, "w", encoding="UTF-8") as fH:
2398                    fH.write(infoText)
2399
2400                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2401
2402        return view
2403
2404    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2405        """
2406        Returns history operations between two given dates for current `accountId`.
2407        If `reportFile` string is not empty then also save human-readable report.
2408        Shows some statistical data of closed positions.
2409
2410        :param start: see docstring in `GetDatesAsString()` method
2411        :param end: see docstring in `GetDatesAsString()` method
2412        :param show: if `True` then also prints all records to the console.
2413        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2414        :return: original list of dictionaries with history of deals records from API ("operations" key):
2415                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2416                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2417        """
2418        if self.accountId is None or not self.accountId:
2419            uLogger.error("Variable `accountId` must be defined for using this method!")
2420            raise Exception("Account ID required")
2421
2422        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2423
2424        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2425
2426        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2427        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2428        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2429        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2430        customStat = {}  # custom statistics in additional to responseJSON
2431
2432        # --- output report in human-readable format:
2433        if show or self.reportFile:
2434            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2435            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2436            nextDay = ""
2437
2438            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2439
2440            if len(ops) > 0:
2441                customStat = {
2442                    "opsCount": 0,  # total operations count
2443                    "buyCount": 0,  # buy operations
2444                    "sellCount": 0,  # sell operations
2445                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2446                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2447                    "payIn": {"rub": 0.},  # Deposit brokerage account
2448                    "payOut": {"rub": 0.},  # Withdrawals
2449                    "divs": {"rub": 0.},  # Dividends income
2450                    "coupons": {"rub": 0.},  # Coupon's income
2451                    "brokerCom": {"rub": 0.},  # Service commissions
2452                    "serviceCom": {"rub": 0.},  # Service commissions
2453                    "marginCom": {"rub": 0.},  # Margin commissions
2454                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2455                }
2456
2457                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2458                for item in ops:
2459                    if item["state"] == "OPERATION_STATE_EXECUTED":
2460                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2461
2462                        # count buy operations:
2463                        if "_BUY" in item["operationType"]:
2464                            customStat["buyCount"] += 1
2465
2466                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2467                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2468
2469                            else:
2470                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2471
2472                        # count sell operations:
2473                        elif "_SELL" in item["operationType"]:
2474                            customStat["sellCount"] += 1
2475
2476                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2477                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2478
2479                            else:
2480                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2481
2482                        # count incoming operations:
2483                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2484                            if item["payment"]["currency"] in customStat["payIn"].keys():
2485                                customStat["payIn"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["payIn"][item["payment"]["currency"]] = payment
2489
2490                        # count withdrawals operations:
2491                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2492                            if item["payment"]["currency"] in customStat["payOut"].keys():
2493                                customStat["payOut"][item["payment"]["currency"]] += payment
2494
2495                            else:
2496                                customStat["payOut"][item["payment"]["currency"]] = payment
2497
2498                        # count dividends income:
2499                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2500                            if item["payment"]["currency"] in customStat["divs"].keys():
2501                                customStat["divs"][item["payment"]["currency"]] += payment
2502
2503                            else:
2504                                customStat["divs"][item["payment"]["currency"]] = payment
2505
2506                        # count coupon's income:
2507                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2508                            if item["payment"]["currency"] in customStat["coupons"].keys():
2509                                customStat["coupons"][item["payment"]["currency"]] += payment
2510
2511                            else:
2512                                customStat["coupons"][item["payment"]["currency"]] = payment
2513
2514                        # count broker commissions:
2515                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2516                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2517                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2518
2519                            else:
2520                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2521
2522                        # count service commissions:
2523                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2524                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2525                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2526
2527                            else:
2528                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2529
2530                        # count margin commissions:
2531                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2532                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2533                                customStat["marginCom"][item["payment"]["currency"]] += payment
2534
2535                            else:
2536                                customStat["marginCom"][item["payment"]["currency"]] = payment
2537
2538                        # count withholding taxes:
2539                        elif "_TAX" in item["operationType"]:
2540                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2541                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2542
2543                            else:
2544                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2545
2546                        else:
2547                            continue
2548
2549                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2550
2551                # --- view "Actions" lines:
2552                info.extend([
2553                    "| Report sections            |                               |                              |                      |                        |\n",
2554                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2555                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2556                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2557                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2558                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2559                    ),
2560                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2561                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2562                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2563                    ),
2564                ])
2565
2566                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2567                for key in opsKeys:
2568                    if key == "rub":
2569                        continue
2570
2571                    info.extend([
2572                        "|                            |                               | {:<28} |                      |                        |\n".format(
2573                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2574                        ),
2575                        "|                            |                               | {:<28} |                      |                        |\n".format(
2576                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2577                        ),
2578                    ])
2579
2580                info.append(splitLine1)
2581
2582                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2583                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2584                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2585                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2587                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2588                    )
2589
2590                # --- view "Payments" lines:
2591                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2592                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2593
2594                for key in paymentsKeys:
2595                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2596
2597                info.append(splitLine1)
2598
2599                # --- view "Commissions and taxes" lines:
2600                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2601                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2602
2603                for key in comKeys:
2604                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2605
2606                info.append(splitLine1)
2607
2608                info.extend([
2609                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2610                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2611                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2612                ])
2613
2614            else:
2615                info.append("Broker returned no operations during this period\n")
2616
2617            # --- view "Operations" section:
2618            for item in ops:
2619                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2620                    continue
2621
2622                else:
2623                    self.figi = item["figi"] if item["figi"] else ""
2624                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2625                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2626
2627                    # group of deals during one day:
2628                    if nextDay and item["date"].split("T")[0] != nextDay:
2629                        info.append(splitLine2)
2630                        nextDay = ""
2631
2632                    else:
2633                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2634
2635                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2636                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2637                        self.figi if self.figi else "—",
2638                        instrument["ticker"] if instrument else "—",
2639                        instrument["type"] if instrument else "—",
2640                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2641                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2642                        TKS_OPERATION_STATES[item["state"]],
2643                        TKS_OPERATION_TYPES[item["operationType"]],
2644                    ))
2645
2646            infoText = "".join(info)
2647
2648            if show:
2649                if self.moreDebug:
2650                    uLogger.debug("Records about history of a client's operations successfully received")
2651
2652                uLogger.info(infoText)
2653
2654            if self.reportFile:
2655                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2656                    fH.write(infoText)
2657
2658                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2659
2660        return ops, customStat
2661
2662    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2663        """
2664        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2665
2666        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2667        Warning! Broker server used ISO UTC time by default.
2668
2669        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2670        Also, `historyFile` used to update history with `onlyMissing` parameter.
2671
2672        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2673
2674        :param start: see docstring in `GetDatesAsString()` method.
2675        :param end: see docstring in `GetDatesAsString()` method.
2676        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2677                         `"hour"`, `"day"`. Default: `"hour"`.
2678        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2679                            False by default. Warning! History appends only from last candle to current time
2680                            with always update last candle!
2681        :param csvSep: separator if csv-file is used, `,` by default.
2682        :param show: if `True` then also prints Pandas DataFrame to the console.
2683        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2684                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2685        """
2686        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2687        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2688        history = None  # empty pandas object for history
2689
2690        if interval not in TKS_CANDLE_INTERVALS.keys():
2691            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2692            raise Exception("Incorrect value")
2693
2694        if not (self.ticker or self.figi):
2695            uLogger.error("Ticker or FIGI must be defined!")
2696            raise Exception("Ticker or FIGI required")
2697
2698        if self.ticker and not self.figi:
2699            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2700            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2701
2702        if self.figi and not self.ticker:
2703            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2704            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2705
2706        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2707        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2708        if interval.lower() != "day":
2709            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2710
2711        delta = dtEnd - dtStart  # current UTC time minus last time in file
2712        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2713
2714        # calculate history length in candles:
2715        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2716        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2717            length += 1  # to avoid fraction time
2718
2719        # calculate data blocks count:
2720        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2721
2722        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2723        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2724        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2725        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2726        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2727
2728        tempOld = None  # pandas object for old history, if --only-missing key present
2729        lastTime = None  # datetime object of last old candle in file
2730
2731        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2732            uLogger.debug("--only-missing key present, add only last missing candles...")
2733            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2734
2735            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2736
2737            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2738            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2739            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2740            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2741
2742            # get last datetime object from last string in file or minus 1 delta if file is empty:
2743            if len(tempOld) > 0:
2744                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2745
2746            else:
2747                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2748
2749            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2750
2751        responseJSONs = []  # raw history blocks of data
2752
2753        blockEnd = dtEnd
2754        for item in range(blocks):
2755            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2756            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2757
2758            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2759                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2760            ))
2761
2762            if blockStart == blockEnd:
2763                uLogger.debug("Skipped this zero-length block...")
2764
2765            else:
2766                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2767                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2768                self.body = str({
2769                    "figi": self.figi,
2770                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2771                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2772                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2773                })
2774                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2775
2776                if "code" in responseJSON.keys():
2777                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2778
2779                else:
2780                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2781                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2782
2783                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2784
2785            blockEnd = blockStart
2786
2787        printCount = len(responseJSONs)  # candles to show in console
2788        if responseJSONs:
2789            tempHistory = pd.DataFrame(
2790                data={
2791                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2792                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2793                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2794                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2795                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2796                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2797                    "volume": [int(item["volume"]) for item in responseJSONs],
2798                },
2799                index=range(len(responseJSONs)),
2800                columns=["date", "time", "open", "high", "low", "close", "volume"],
2801            )
2802            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2803            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2804
2805            # append only newest candles to old history if --only-missing key present:
2806            if onlyMissing and tempOld is not None and lastTime is not None:
2807                index = 0  # find start index in tempHistory data:
2808
2809                for i, item in tempHistory.iterrows():
2810                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2811
2812                    if curTime == lastTime:
2813                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2814                        index = i
2815                        printCount = index + 1
2816                        break
2817
2818                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2819
2820            else:
2821                history = tempHistory  # if no `--only-missing` key then load full data from server
2822
2823            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2824
2825        if history is not None and not history.empty:
2826            if show:
2827                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2828                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2829                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2830                ))
2831
2832        else:
2833            uLogger.warning("Received an empty candles history!")
2834
2835        if self.historyFile is not None:
2836            if history is not None and not history.empty:
2837                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2838                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2839
2840            else:
2841                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2842
2843        else:
2844            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2845
2846        return history
2847
2848    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2849        """
2850        Load candles history from csv-file and return Pandas DataFrame object.
2851
2852        See also: `History()` and `ShowHistoryChart()` methods.
2853
2854        :param filePath: path to csv-file to open.
2855        """
2856        loadedHistory = None  # init candles data object
2857
2858        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2859
2860        if os.path.exists(filePath):
2861            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2862
2863            tfStr = self.priceModel.FormattedDelta(
2864                self.priceModel.timeframe,
2865                "{days} days {hours}h {minutes}m {seconds}s",
2866            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2867                self.priceModel.timeframe,
2868                "{hours}h {minutes}m {seconds}s",
2869            )
2870
2871            if loadedHistory is not None and not loadedHistory.empty:
2872                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2873                    len(loadedHistory),
2874                    tfStr,
2875                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2876                )
2877
2878            else:
2879                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2880
2881        else:
2882            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2883
2884        return loadedHistory
2885
2886    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2887        """
2888        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2889
2890        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2891        Default: `index.html` (both for interact and non-interact candlesticks chart).
2892
2893        See also: `History()` and `LoadHistory()` methods.
2894
2895        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2896        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2897                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2898                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2899                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2900        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2901                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2902        """
2903        if isinstance(candles, str):
2904            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2905            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2906
2907        elif isinstance(candles, pd.DataFrame):
2908            self.priceModel.prices = candles  # set candles chain from variable
2909            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2910
2911            if "datetime" not in candles.columns:
2912                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2913
2914        else:
2915            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2916            raise Exception("Incorrect value")
2917
2918        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2919
2920        if interact:
2921            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2922
2923            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2924
2925        else:
2926            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2927
2928            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2929
2930        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2931
2932    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2933        """
2934        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2935        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2936
2937        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2938
2939        :param operation: string "Buy" or "Sell".
2940        :param lots: volume, integer count of lots >= 1.
2941        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2942        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2943        :param expDate: string "Undefined" by default or local date in future,
2944                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2945        :return: JSON with response from broker server.
2946        """
2947        if self.accountId is None or not self.accountId:
2948            uLogger.error("Variable `accountId` must be defined for using this method!")
2949            raise Exception("Account ID required")
2950
2951        if operation is None or not operation or operation not in ("Buy", "Sell"):
2952            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2953            raise Exception("Incorrect value")
2954
2955        if lots is None or lots < 1:
2956            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2957            lots = 1
2958
2959        if tp is None or tp < 0:
2960            tp = 0
2961
2962        if sl is None or sl < 0:
2963            sl = 0
2964
2965        if expDate is None or not expDate:
2966            expDate = "Undefined"
2967
2968        if not (self.ticker or self.figi):
2969            uLogger.error("Ticker or FIGI must be defined!")
2970            raise Exception("Ticker or FIGI required")
2971
2972        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2973        self.ticker = instrument["ticker"]
2974        self.figi = instrument["figi"]
2975
2976        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2977
2978        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2979        self.body = str({
2980            "figi": self.figi,
2981            "quantity": str(lots),
2982            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2983            "accountId": str(self.accountId),
2984            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2985        })
2986        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2987
2988        if "orderId" in response.keys():
2989            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2990                operation, response["orderId"],
2991                self.ticker, self.figi, lots,
2992                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2993                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2994                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2995            ))
2996
2997            if tp > 0:
2998                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2999
3000            if sl > 0:
3001                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3002
3003        else:
3004            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3005
3006        return response
3007
3008    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3009        """
3010        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3011        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3012
3013        See also: `Order()` and `Trade()` docstrings.
3014
3015        :param lots: volume, integer count of lots >= 1.
3016        :param tp: float > 0, take profit price of stop-order.
3017        :param sl: float > 0, stop loss price of stop-order.
3018        :param expDate: it's a local date in future.
3019                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3020        :return: JSON with response from broker server.
3021        """
3022        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3023
3024    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3025        """
3026        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3027        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3028
3029        See also: `Order()` and `Trade()` docstrings.
3030
3031        :param lots: volume, integer count of lots >= 1.
3032        :param tp: float > 0, take profit price of stop-order.
3033        :param sl: float > 0, stop loss price of stop-order.
3034        :param expDate: it's a local date in the future.
3035                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3036        :return: JSON with response from broker server.
3037        """
3038        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3039
3040    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3041        """
3042        Close position of given instruments.
3043
3044        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3045        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3046                         This avoids unnecessary downloading data from the server.
3047        """
3048        if instruments is None or not instruments:
3049            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3050            raise Exception("Ticker or FIGI required")
3051
3052        if isinstance(instruments, str):
3053            instruments = [instruments]
3054
3055        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3056        if uniqueInstruments:
3057            if portfolio is None or not portfolio:
3058                portfolio = self.Overview(show=False)
3059
3060            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3061            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3062
3063            for self.figi in uniqueInstruments:
3064                if self.figi not in allOpened:
3065                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3066                    continue
3067
3068                # search open trade info about instrument by ticker:
3069                instrument = {}
3070                for iType in TKS_INSTRUMENTS:
3071                    if instrument:
3072                        break
3073
3074                    for item in portfolio["stat"][iType]:
3075                        if item["figi"] == self.figi:
3076                            instrument = item
3077                            break
3078
3079                if instrument:
3080                    self.ticker = instrument["ticker"]
3081                    self.figi = instrument["figi"]
3082
3083                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3084                        self.ticker,
3085                        self.figi,
3086                        int(instrument["volume"]),
3087                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3088                    ))
3089
3090                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3091
3092                    if tradeLots > 0:
3093                        if instrument["blocked"] > 0:
3094                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3095                                instrument["blocked"],
3096                                self.ticker,
3097                                tradeLots,
3098                            ))
3099
3100                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3101                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3102
3103                    else:
3104                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3105
3106    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3107        """
3108        Close all positions of given instruments with defined type.
3109
3110        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3111        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3112                         This avoids unnecessary downloading data from the server.
3113        """
3114        if iType not in TKS_INSTRUMENTS:
3115            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3116
3117        else:
3118            if portfolio is None or not portfolio:
3119                portfolio = self.Overview(show=False)
3120
3121            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3122            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3123
3124            if tickers and portfolio:
3125                self.CloseTrades(tickers, portfolio)
3126
3127            else:
3128                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3129
3130    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3131        """
3132        Universal method to create market or limit orders with all available parameters for current `accountId`.
3133        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3134
3135        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3136        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3137
3138        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3139        then broker immediately open market order as you can do simple --buy or --sell operations!
3140
3141        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3142        When current price will go up or down to target price value then broker opens a limit order.
3143        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3144
3145        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3146
3147        :param operation: string "Buy" or "Sell".
3148        :param orderType: string "Limit" or "Stop".
3149        :param lots: volume, integer count of lots >= 1.
3150        :param targetPrice: target price > 0. This is open trade price for limit order.
3151        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3152                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3153        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3154                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3155                         Stop loss order always executed by market price.
3156        :param expDate: string "Undefined" by default or local date in future.
3157                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3158                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3159                        A limit order has no expiration date, it lasts until the end of the trading day.
3160        :return: JSON with response from broker server.
3161        """
3162        if self.accountId is None or not self.accountId:
3163            uLogger.error("Variable `accountId` must be defined for using this method!")
3164            raise Exception("Account ID required")
3165
3166        if operation is None or not operation or operation not in ("Buy", "Sell"):
3167            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3168            raise Exception("Incorrect value")
3169
3170        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3171            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3172            raise Exception("Incorrect value")
3173
3174        if lots is None or lots < 1:
3175            uLogger.error("You must define trade volume > 0: integer count of lots!")
3176            raise Exception("Incorrect value")
3177
3178        if targetPrice is None or targetPrice <= 0:
3179            uLogger.error("Target price for limit-order must be greater than 0!")
3180            raise Exception("Incorrect value")
3181
3182        if limitPrice is None or limitPrice <= 0:
3183            limitPrice = targetPrice
3184
3185        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3186            stopType = "Limit"
3187
3188        if expDate is None or not expDate:
3189            expDate = "Undefined"
3190
3191        if not (self.ticker or self.figi):
3192            uLogger.error("Tocker or FIGI must be defined!")
3193            raise Exception("Ticker or FIGI required")
3194
3195        response = {}
3196        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3197        self.ticker = instrument["ticker"]
3198        self.figi = instrument["figi"]
3199
3200        if orderType == "Limit":
3201            uLogger.debug(
3202                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3203                    self.ticker, self.figi,
3204                    operation, lots, targetPrice, instrument["currency"],
3205                ))
3206
3207            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3208            self.body = str({
3209                "figi": self.figi,
3210                "quantity": str(lots),
3211                "price": FloatToNano(targetPrice),
3212                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3213                "accountId": str(self.accountId),
3214                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3215            })
3216            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3217
3218            if "orderId" in response.keys():
3219                uLogger.info(
3220                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3221                        response["orderId"],
3222                        self.ticker, self.figi,
3223                        operation, lots, targetPrice, instrument["currency"],
3224                    ))
3225
3226                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3227                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3228                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3229                            targetPrice, instrument["currency"],
3230                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3231                        ))
3232
3233                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3234                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3235                            targetPrice, instrument["currency"],
3236                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3237                        ))
3238
3239            else:
3240                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3241
3242        if orderType == "Stop":
3243            uLogger.debug(
3244                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3245                    self.ticker, self.figi,
3246                    operation, lots,
3247                    targetPrice, instrument["currency"],
3248                    limitPrice, instrument["currency"],
3249                    stopType, expDate,
3250                ))
3251
3252            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3253            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3254            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3255
3256            body = {
3257                "figi": self.figi,
3258                "quantity": str(lots),
3259                "price": FloatToNano(limitPrice),
3260                "stopPrice": FloatToNano(targetPrice),
3261                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3262                "accountId": str(self.accountId),
3263                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3264                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3265            }
3266
3267            if expDateUTC:
3268                body["expireDate"] = expDateUTC
3269
3270            self.body = str(body)
3271            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3272
3273            if "stopOrderId" in response.keys():
3274                uLogger.info(
3275                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3276                        response["stopOrderId"],
3277                        self.ticker, self.figi,
3278                        operation, lots,
3279                        targetPrice, instrument["currency"],
3280                        limitPrice, instrument["currency"],
3281                        TKS_STOP_ORDER_TYPES[stopOrderType],
3282                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3283                    ))
3284
3285                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3286                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3287                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3288                            targetPrice, instrument["currency"],
3289                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3290                        ))
3291
3292                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3293                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3294                            targetPrice, instrument["currency"],
3295                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3296                        ))
3297
3298            else:
3299                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3300
3301        return response
3302
3303    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3304        """
3305        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3306        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3307        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3308        See also: `Order()` docstring.
3309
3310        :param lots: volume, integer count of lots >= 1.
3311        :param targetPrice: target price > 0. This is open trade price for limit order.
3312        :return: JSON with response from broker server.
3313        """
3314        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3315
3316    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3317        """
3318        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3319        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3320        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3321        target price value then broker opens a limit order. See also: `Order()` docstring.
3322
3323        :param lots: volume, integer count of lots >= 1.
3324        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3325        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3326                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3327        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3328                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3329        :param expDate: string "Undefined" by default or local date in future.
3330                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3331                        This date is converting to UTC format for server.
3332        :return: JSON with response from broker server.
3333        """
3334        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3335
3336    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3337        """
3338        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3339        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3340        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3341        See also: `Order()` docstring.
3342
3343        :param lots: volume, integer count of lots >= 1.
3344        :param targetPrice: target price > 0. This is open trade price for limit order.
3345        :return: JSON with response from broker server.
3346        """
3347        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3348
3349    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3350        """
3351        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3352        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3353        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3354        target price value then broker opens a limit order. See also: `Order()` docstring.
3355
3356        :param lots: volume, integer count of lots >= 1.
3357        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3358        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3359                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3360        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3361                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3362        :param expDate: string "Undefined" by default or local date in future.
3363                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3364                        This date is converting to UTC format for server.
3365        :return: JSON with response from broker server.
3366        """
3367        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3368
3369    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3370        """
3371        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3372
3373        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3374        :param allOrdersIDs: pre-received lists of all active pending orders.
3375                             This avoids unnecessary downloading data from the server.
3376        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3377        """
3378        if self.accountId is None or not self.accountId:
3379            uLogger.error("Variable `accountId` must be defined for using this method!")
3380            raise Exception("Account ID required")
3381
3382        if orderIDs:
3383            if allOrdersIDs is None or not allOrdersIDs:
3384                rawOrders = self.RequestPendingOrders()
3385                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3386
3387            if allStopOrdersIDs is None or not allStopOrdersIDs:
3388                rawStopOrders = self.RequestStopOrders()
3389                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3390
3391            for orderID in orderIDs:
3392                idInPendingOrders = orderID in allOrdersIDs
3393                idInStopOrders = orderID in allStopOrdersIDs
3394
3395                if not (idInPendingOrders or idInStopOrders):
3396                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3397                    continue
3398
3399                else:
3400                    if idInPendingOrders:
3401                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3402
3403                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3404                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3405                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3406                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3407
3408                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3409                            if self.moreDebug:
3410                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3411
3412                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3413
3414                        else:
3415                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3416
3417                    elif idInStopOrders:
3418                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3419
3420                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3421                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3422                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3423                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3424
3425                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3426                            if self.moreDebug:
3427                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3428
3429                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3430
3431                        else:
3432                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3433
3434                    else:
3435                        continue
3436
3437    def CloseAllOrders(self) -> None:
3438        """
3439        Gets a list of open pending and stop orders and cancel it all.
3440        """
3441        rawOrders = self.RequestPendingOrders()
3442        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3443        lenOrders = len(allOrdersIDs)
3444
3445        rawStopOrders = self.RequestStopOrders()
3446        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3447        lenSOrders = len(allStopOrdersIDs)
3448
3449        if lenOrders > 0 or lenSOrders > 0:
3450            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3451
3452            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3453
3454        else:
3455            uLogger.info("Orders not found, nothing to cancel.")
3456
3457    def CloseAll(self, *args) -> None:
3458        """
3459        Close all available (not blocked) opened trades and orders.
3460
3461        Also, you can select one or more keywords case-insensitive:
3462        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3463
3464        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3465        """
3466        overview = self.Overview(show=False)  # get all open trades info
3467
3468        if len(args) == 0:
3469            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3470            self.CloseAllOrders()  # close all pending and stop orders
3471
3472            for iType in TKS_INSTRUMENTS:
3473                if iType != "Currencies":
3474                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3475
3476        else:
3477            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3478            lowerArgs = [x.lower() for x in args]
3479
3480            if "orders" in lowerArgs:
3481                self.CloseAllOrders()  # close all pending and stop orders
3482
3483            for iType in TKS_INSTRUMENTS:
3484                if iType.lower() in lowerArgs and iType != "Currencies":
3485                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3486
3487    @staticmethod
3488    def ParseOrderParameters(operation, **inputParameters):
3489        """
3490        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3491
3492        :param operation: string "Buy" or "Sell".
3493        :param inputParameters: this is dict of strings that looks like this
3494               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3495               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3496               "prices" key: one or more prices to open limit-orders
3497               Counts of values in lots and prices lists must be equals!
3498        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3499        """
3500        # TODO: update order grid work with api v2
3501        pass
3502        # uLogger.debug("Input parameters: {}".format(inputParameters))
3503        #
3504        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3505        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3506        #     raise Exception("Incorrect value")
3507        #
3508        # if "l" in inputParameters.keys():
3509        #     inputParameters["lots"] = inputParameters.pop("l")
3510        #
3511        # if "p" in inputParameters.keys():
3512        #     inputParameters["prices"] = inputParameters.pop("p")
3513        #
3514        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3515        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3516        #     raise Exception("Incorrect value")
3517        #
3518        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3519        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3520        #
3521        # if len(lots) != len(prices):
3522        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3523        #     raise Exception("Incorrect value")
3524        #
3525        # uLogger.debug("Extracted parameters for orders:")
3526        # uLogger.debug("lots = {}".format(lots))
3527        # uLogger.debug("prices = {}".format(prices))
3528        #
3529        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3530        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3531        # uLogger.debug("Order parameters: {}".format(result))
3532        #
3533        # return result
3534
3535    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3536        """
3537        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3538
3539        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3540        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3541        """
3542        result = False
3543        msg = "Instrument not defined!"
3544
3545        if portfolio is None or not portfolio:
3546            portfolio = self.Overview(show=False)
3547
3548        if self.ticker:
3549            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3550            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3551
3552            for iType in TKS_INSTRUMENTS:
3553                for instrument in portfolio["stat"][iType]:
3554                    if instrument["ticker"] == self.ticker:
3555                        result = True
3556                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3557                        break
3558
3559        elif self.figi:
3560            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3561            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3562
3563            for iType in TKS_INSTRUMENTS:
3564                for instrument in portfolio["stat"][iType]:
3565                    if instrument["figi"] == self.figi:
3566                        result = True
3567                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3568                        break
3569
3570        else:
3571            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3572
3573        uLogger.debug(msg)
3574
3575        return result
3576
3577    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3578        """
3579        Returns instrument from the user's portfolio if it presents there.
3580        Instrument must be defined by `ticker` (highly priority) or `figi`.
3581
3582        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3583        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3584        """
3585        result = None
3586        msg = "Instrument not defined!"
3587
3588        if portfolio is None or not portfolio:
3589            portfolio = self.Overview(show=False)
3590
3591        if self.ticker:
3592            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3593            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3594
3595            for iType in TKS_INSTRUMENTS:
3596                for instrument in portfolio["stat"][iType]:
3597                    if instrument["ticker"] == self.ticker:
3598                        result = instrument
3599                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3600                        break
3601
3602        elif self.figi:
3603            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3604            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3605
3606            for iType in TKS_INSTRUMENTS:
3607                for instrument in portfolio["stat"][iType]:
3608                    if instrument["figi"] == self.figi:
3609                        result = instrument
3610                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3611                        break
3612
3613        else:
3614            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3615
3616        uLogger.debug(msg)
3617
3618        return result
3619
3620    def RequestLimits(self) -> dict:
3621        """
3622        Method for obtaining the available funds for withdrawal for current `accountId`.
3623
3624        See also:
3625        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3626        - `OverviewLimits()` method
3627
3628        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3629                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3630                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3631                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3632        """
3633        if self.accountId is None or not self.accountId:
3634            uLogger.error("Variable `accountId` must be defined for using this method!")
3635            raise Exception("Account ID required")
3636
3637        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3638
3639        self.body = str({"accountId": self.accountId})
3640        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3641        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3642
3643        if self.moreDebug:
3644            uLogger.debug("Records about available funds for withdrawal successfully received")
3645
3646        return rawLimits
3647
3648    def OverviewLimits(self, show: bool = False) -> dict:
3649        """
3650        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3651
3652        See also: `RequestLimits()`.
3653
3654        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3655        :return: dict with raw parsed data from server and some calculated statistics about it.
3656        """
3657        if self.accountId is None or not self.accountId:
3658            uLogger.error("Variable `accountId` must be defined for using this method!")
3659            raise Exception("Account ID required")
3660
3661        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3662
3663        view = {
3664            "rawLimits": rawLimits,
3665            "limits": {  # parsed data for every currency:
3666                "money": {  # this is an array of portfolio currency positions
3667                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3668                },
3669                "blocked": {  # this is an array of blocked currency
3670                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3671                },
3672                "blockedGuarantee": {  # this is locked money under collateral for futures
3673                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3674                },
3675            },
3676        }
3677
3678        # --- Prepare text table with limits in human-readable format:
3679        if show:
3680            info = [
3681                "# Withdrawal limits\n\n",
3682                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3683                "* **Account ID:** [{}]\n".format(self.accountId),
3684            ]
3685
3686            if view["limits"]["money"]:
3687                info.extend([
3688                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3689                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3690                ])
3691
3692            else:
3693                info.append("\nNo withdrawal limits\n")
3694
3695            for curr in view["limits"]["money"].keys():
3696                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3697                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3698                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3699
3700                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3701                    "[{}]".format(curr),
3702                    "{:.2f}".format(view["limits"]["money"][curr]),
3703                    "{:.2f}".format(availableMoney),
3704                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3705                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3706                )
3707
3708                if curr == "rub":
3709                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3710
3711                else:
3712                    info.append(infoStr)
3713
3714            infoText = "".join(info)
3715
3716            uLogger.info(infoText)
3717
3718            if self.withdrawalLimitsFile:
3719                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3720                    fH.write(infoText)
3721
3722                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3723
3724        return view
3725
3726    def RequestAccounts(self) -> dict:
3727        """
3728        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3729
3730        See also:
3731        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3732        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3733        - `OverviewUserInfo()` method
3734
3735        :return: dict with raw data from server that contains accounts info. Example of dict:
3736                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3737                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3738                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3739                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3740        """
3741        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3742
3743        self.body = str({})
3744        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3745        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3746
3747        if self.moreDebug:
3748            uLogger.debug("Records about available accounts successfully received")
3749
3750        return rawAccounts
3751
3752    def RequestUserInfo(self) -> dict:
3753        """
3754        Method for requesting common user's information.
3755
3756        See also:
3757        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3758        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3759        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3760        - `OverviewUserInfo()` method
3761
3762        :return: dict with raw data from server that contains user's information. Example of dict:
3763                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3764                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3765        """
3766        uLogger.debug("Requesting common user's information. Wait, please...")
3767
3768        self.body = str({})
3769        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3770        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3771
3772        if self.moreDebug:
3773            uLogger.debug("Records about current user successfully received")
3774
3775        return rawUserInfo
3776
3777    def RequestMarginStatus(self, accountId: str = None) -> dict:
3778        """
3779        Method for requesting margin calculation for defined account ID.
3780
3781        See also:
3782        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3783        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3784        - `OverviewUserInfo()` method
3785
3786        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3787        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3788                 Example of responses:
3789                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3790                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3791                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3792                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3793                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3794                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3795        """
3796        if accountId is None or not accountId:
3797            if self.accountId is None or not self.accountId:
3798                uLogger.error("Variable `accountId` must be defined for using this method!")
3799                raise Exception("Account ID required")
3800
3801            else:
3802                accountId = self.accountId  # use `self.accountId` (main ID) by default
3803
3804        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3805
3806        self.body = str({"accountId": accountId})
3807        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3808        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3809
3810        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3811            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3812            rawMargin = {}
3813
3814        else:
3815            if self.moreDebug:
3816                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3817
3818        return rawMargin
3819
3820    def RequestTariffLimits(self) -> dict:
3821        """
3822        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3823
3824        See also:
3825        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3826        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3827        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3828        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3829        - `OverviewUserInfo()` method
3830
3831        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3832                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3833                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3834        """
3835        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3836
3837        self.body = str({})
3838        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3839        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3840
3841        if self.moreDebug:
3842            uLogger.debug("Records with limits of current tariff successfully received")
3843
3844        return rawTariffLimits
3845
3846    def RequestBondCoupons(self, iJSON: dict) -> dict:
3847        """
3848        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3849        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3850        All dates are in UTC timezone.
3851
3852        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3853        Documentation:
3854        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3855        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3856
3857        See also: `ExtendBondsData()`.
3858
3859        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3860                      If raw iJSON is not data of bond then server returns an error [400] with message:
3861                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3862        :return: dictionary with bond payment calendar. Response example
3863                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3864                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3865                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3866                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3867        """
3868        if iJSON["figi"] is None or not iJSON["figi"]:
3869            uLogger.error("FIGI must be defined for using this method!")
3870            raise Exception("FIGI required")
3871
3872        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3873        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3874
3875        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3876            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3877            self.figi,
3878            startDate,
3879            endDate,
3880        ))
3881
3882        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3883        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3884        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3885
3886        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3887            uLogger.warning("Instrument type is not bond!")
3888
3889        else:
3890            if self.moreDebug:
3891                uLogger.debug("Records about bond payment calendar successfully received")
3892
3893        return calendar
3894
3895    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3896        """
3897        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3898        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3899        coupon yields, current yields and some statistics etc.
3900
3901        WARNING! This is too long operation if a lot of bonds requested from broker server.
3902
3903        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3904
3905        :param instruments: list of strings with tickers or FIGIs.
3906        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3907                     for further used by data scientists or stock analytics.
3908        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3909                 In XLSX-file and Pandas DataFrame fields mean:
3910                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3911                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3912        """
3913        if instruments is None or not instruments:
3914            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3915            raise Exception("Ticker or FIGI required")
3916
3917        if isinstance(instruments, str):
3918            instruments = [instruments]
3919
3920        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3921
3922        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3923
3924        iCount = len(uniqueInstruments)
3925        tooLong = iCount >= 20
3926        if tooLong:
3927            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3928
3929        bonds = None
3930        for i, self.figi in enumerate(uniqueInstruments):
3931            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3932
3933            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3934                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3935                rawBond = self.SearchByFIGI(requestPrice=True)
3936
3937                # Widen raw data with UTC current time (iData["actualDateTime"]):
3938                actualDate = datetime.now(tzutc())
3939                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3940
3941                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3942                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3943
3944                # Replace some values with human-readable:
3945                iData["nominalCurrency"] = iData["nominal"]["currency"]
3946                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3947                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3948                iData["aciCurrency"] = iData["aciValue"]["currency"]
3949                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3950                iData["issueSize"] = int(iData["issueSize"])
3951                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3952                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3953                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3954                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3955                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3956                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3957                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3958                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3959                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3960                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3961
3962                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3963                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3964                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3965                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3966                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3967                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3968                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3969                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3970                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3971                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3972                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3973
3974                # Widen raw data with calendar data from `rawCalendar` values:
3975                calendarData = []
3976                if "events" in iData["rawCalendar"].keys():
3977                    for item in iData["rawCalendar"]["events"]:
3978                        calendarData.append({
3979                            "couponDate": item["couponDate"],
3980                            "couponNumber": int(item["couponNumber"]),
3981                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3982                            "payCurrency": item["payOneBond"]["currency"],
3983                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3984                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3985                            "couponStartDate": item["couponStartDate"],
3986                            "couponEndDate": item["couponEndDate"],
3987                            "couponPeriod": item["couponPeriod"],
3988                        })
3989
3990                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3991                    if "maturityDate" not in iData.keys():
3992                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3993
3994                # Widen raw data with Coupon Rate.
3995                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3996                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3997                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3998                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3999
4000                # Widen raw data with Yield to Maturity (YTM) on current date.
4001                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4002                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4003                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4004                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4005                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4006                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4007
4008                iData["calendar"] = calendarData  # adds calendar at the end
4009
4010                # Remove not used data:
4011                iData.pop("uid")
4012                iData.pop("positionUid")
4013                iData.pop("currentPrice")
4014                iData.pop("rawCalendar")
4015
4016                colNames = list(iData.keys())
4017                if bonds is None:
4018                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4019
4020                else:
4021                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4022
4023            else:
4024                uLogger.warning("Instrument is not a bond!")
4025
4026            processed = round(100 * (i + 1) / iCount, 1)
4027            if tooLong and processed % 5 == 0:
4028                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4029
4030            else:
4031                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4032
4033        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4034
4035        # Saving bonds from Pandas DataFrame to XLSX sheet:
4036        if xlsx and self.bondsXLSXFile:
4037            with pd.ExcelWriter(
4038                    path=self.bondsXLSXFile,
4039                    date_format=TKS_DATE_FORMAT,
4040                    datetime_format=TKS_DATE_TIME_FORMAT,
4041                    mode="w",
4042            ) as writer:
4043                bonds.to_excel(
4044                    writer,
4045                    sheet_name="Extended bonds data",
4046                    index=True,
4047                    encoding="UTF-8",
4048                    freeze_panes=(1, 1),
4049                )  # saving as XLSX-file with freeze first row and column as headers
4050
4051            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4052
4053        return bonds
4054
4055    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4056        """
4057        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4058
4059        WARNING! This is too long operation if a lot of bonds requested from broker server.
4060
4061        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4062
4063        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4064                        extended information about bonds: main info, current prices, bond payment calendar,
4065                        coupon yields, current yields and some statistics etc.
4066                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4067        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4068                     for further used by data scientists or stock analytics.
4069        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4070        """
4071        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4072            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4073
4074        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4075
4076        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4077        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4078        calendar = None
4079        for bond in extBonds.iterrows():
4080            for item in bond[1]["calendar"]:
4081                cData = {
4082                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4083                    "couponDate": item["couponDate"],
4084                    "figi": bond[1]["figi"],
4085                    "ticker": bond[1]["ticker"],
4086                    "name": bond[1]["name"],
4087                    "couponNumber": item["couponNumber"],
4088                    "payOneBond": item["payOneBond"],
4089                    "payCurrency": item["payCurrency"],
4090                    "couponType": item["couponType"],
4091                    "couponPeriod": item["couponPeriod"],
4092                    "fixDate": item["fixDate"],
4093                    "couponStartDate": item["couponStartDate"],
4094                    "couponEndDate": item["couponEndDate"],
4095                }
4096
4097                if calendar is None:
4098                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4099
4100                else:
4101                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4102
4103        if calendar is not None:
4104            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4105
4106            # Saving calendar from Pandas DataFrame to XLSX sheet:
4107            if xlsx:
4108                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4109
4110                with pd.ExcelWriter(
4111                        path=xlsxCalendarFile,
4112                        date_format=TKS_DATE_FORMAT,
4113                        datetime_format=TKS_DATE_TIME_FORMAT,
4114                        mode="w",
4115                ) as writer:
4116                    humanReadable = calendar.copy(deep=True)
4117                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4118                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4119                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4120                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4121                    humanReadable.columns = colNames  # human-readable column names
4122
4123                    humanReadable.to_excel(
4124                        writer,
4125                        sheet_name="Bond payments calendar",
4126                        index=False,
4127                        encoding="UTF-8",
4128                        freeze_panes=(1, 2),
4129                    )  # saving as XLSX-file with freeze first row and column as headers
4130
4131                    del humanReadable  # release df in memory
4132
4133                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4134
4135        return calendar
4136
4137    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4138        """
4139        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4140        Also, creates Markdown file with calendar data, `calendar.md` by default.
4141
4142        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4143
4144        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4145                        extended information about bonds: main info, current prices, bond payment calendar,
4146                        coupon yields, current yields and some statistics etc.
4147                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4148        :param show: if `True` then also printing bonds payment calendar to the console,
4149                     otherwise save to file `calendarFile` only. `False` by default.
4150        :return: multilines text in Markdown format with bonds payment calendar as a table.
4151        """
4152        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4153            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4154
4155        infoText = "# Bond payments calendar\n\n"
4156
4157        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4158
4159        if not (calendar is None or calendar.empty):
4160            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4161
4162            info = [
4163                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4164                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4165            ]
4166
4167            newMonth = False
4168            notOneBond = calendar["figi"].nunique() > 1
4169            for i, bond in enumerate(calendar.iterrows()):
4170                if newMonth and notOneBond:
4171                    info.append(splitLine)
4172
4173                info.append(
4174                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4175                        "  √" if bond[1]["paid"] else "  —",
4176                        bond[1]["couponDate"].split("T")[0],
4177                        bond[1]["figi"],
4178                        bond[1]["ticker"],
4179                        bond[1]["couponNumber"],
4180                        "{} {}".format(
4181                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4182                            bond[1]["payCurrency"],
4183                        ),
4184                        bond[1]["couponType"],
4185                        bond[1]["couponPeriod"],
4186                        bond[1]["fixDate"].split("T")[0],
4187                    )
4188                )
4189
4190                if i < len(calendar.values) - 1:
4191                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4192                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4193                    newMonth = False if curDate.month == nextDate.month else True
4194
4195                else:
4196                    newMonth = False
4197
4198            infoText += "".join(info)
4199
4200            if show:
4201                uLogger.info("{}".format(infoText))
4202
4203            if self.calendarFile is not None:
4204                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4205                    fH.write(infoText)
4206
4207                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4208
4209        else:
4210            infoText += "No data\n"
4211
4212        return infoText
4213
4214    def OverviewAccounts(self, show: bool = False) -> dict:
4215        """
4216        Method for parsing and show simple table with all available user accounts.
4217
4218        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4219
4220        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4221        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4222                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4223                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4224                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4225                                                        "closed": "—", "access": "Full access" }, ...}}`
4226        """
4227        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4228
4229        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4230        accounts = {
4231            item["id"]: {
4232                "type": TKS_ACCOUNT_TYPES[item["type"]],
4233                "name": item["name"],
4234                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4235                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4236                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4237                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4238            } for item in rawAccounts["accounts"]
4239        }
4240
4241        # Raw and parsed data with some fields replaced in "stat" section:
4242        view = {
4243            "rawAccounts": rawAccounts,
4244            "stat": accounts,
4245        }
4246
4247        # --- Prepare simple text table with only accounts data in human-readable format:
4248        if show:
4249            info = [
4250                "# User accounts\n\n",
4251                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4252                "| Account ID   | Type                      | Status                    | Name                           |\n",
4253                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4254            ]
4255
4256            for account in view["stat"].keys():
4257                info.extend([
4258                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4259                        account,
4260                        view["stat"][account]["type"],
4261                        view["stat"][account]["status"],
4262                        view["stat"][account]["name"],
4263                    )
4264                ])
4265
4266            infoText = "".join(info)
4267
4268            uLogger.info(infoText)
4269
4270            if self.userAccountsFile:
4271                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4272                    fH.write(infoText)
4273
4274                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4275
4276        return view
4277
4278    def OverviewUserInfo(self, show: bool = False) -> dict:
4279        """
4280        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4281
4282        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4283
4284        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4285        :return: dict with raw parsed data from server and some calculated statistics about it.
4286        """
4287        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4288        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4289        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4290        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4291        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4292        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4293
4294        # This is dict with parsed common user data:
4295        userInfo = {
4296            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4297            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4298            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4299            "tariff": rawUserInfo["tariff"],
4300        }
4301
4302        # This is an array of dict with parsed margin statuses for every account IDs:
4303        margins = {}
4304        for accountId in accounts.keys():
4305            if rawMargins[accountId]:
4306                margins[accountId] = {
4307                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4308                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4309                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4310                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4311                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4312                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4313                }
4314
4315            else:
4316                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4317
4318        unary = {}  # unary-connection limits
4319        for item in rawTariffLimits["unaryLimits"]:
4320            if item["limitPerMinute"] in unary.keys():
4321                unary[item["limitPerMinute"]].extend(item["methods"])
4322
4323            else:
4324                unary[item["limitPerMinute"]] = item["methods"]
4325
4326        stream = {}  # stream-connection limits
4327        for item in rawTariffLimits["streamLimits"]:
4328            if item["limit"] in stream.keys():
4329                stream[item["limit"]].extend(item["streams"])
4330
4331            else:
4332                stream[item["limit"]] = item["streams"]
4333
4334        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4335        limits = {
4336            "unary": unary,
4337            "stream": stream,
4338        }
4339
4340        # Raw and parsed data as an output result:
4341        view = {
4342            "rawUserInfo": rawUserInfo,
4343            "rawAccounts": rawAccounts,
4344            "rawMargins": rawMargins,
4345            "rawTariffLimits": rawTariffLimits,
4346            "stat": {
4347                "userInfo": userInfo,
4348                "accounts": accounts,
4349                "margins": margins,
4350                "limits": limits,
4351            },
4352        }
4353
4354        # --- Prepare text table with user information in human-readable format:
4355        if show:
4356            info = [
4357                "# Full user information\n\n",
4358                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4359                "## Common information\n\n",
4360                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4361                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4362                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4363                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4364                "\n## User accounts\n\n",
4365            ]
4366
4367            for account in view["stat"]["accounts"].keys():
4368                info.extend([
4369                    "### ID: [{}]\n\n".format(account),
4370                    "| Parameters           | Values                                                       |\n",
4371                    "|----------------------|--------------------------------------------------------------|\n",
4372                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4373                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4374                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4375                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4376                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4377                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4378                ])
4379
4380                if margins[account]:
4381                    info.extend([
4382                        "| Margin status:       | Enabled                                                      |\n",
4383                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4384                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4385                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4386                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4387                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4388                    ])
4389
4390                else:
4391                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4392
4393            info.extend([
4394                "\n## Current user tariff limits\n",
4395                "\nSee also:\n",
4396                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4397                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4398                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4399                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4400                "\n### Unary limits\n",
4401            ])
4402
4403            if unary:
4404                for key, values in sorted(unary.items()):
4405                    info.append("\n* Max requests per minute: {}\n".format(key))
4406
4407                    for value in values:
4408                        info.append("  - {}\n".format(value))
4409
4410            else:
4411                info.append("\nNot available\n")
4412
4413            info.append("\n### Stream limits\n")
4414
4415            if stream:
4416                for key, values in sorted(stream.items()):
4417                    info.append("\n* Max stream connections: {}\n".format(key))
4418
4419                    for value in values:
4420                        info.append("  - {}\n".format(value))
4421
4422            else:
4423                info.append("\nNot available\n")
4424
4425            infoText = "".join(info)
4426
4427            uLogger.info(infoText)
4428
4429            if self.userInfoFile:
4430                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4431                    fH.write(infoText)
4432
4433                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4434
4435        return view
4436
4437
4438class Args:
4439    """
4440    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4441    """
4442    def __init__(self, **kwargs):
4443        self.__dict__.update(kwargs)
4444
4445    def __getattr__(self, item):
4446        return None
4447
4448
4449def ParseArgs():
4450    """This function get and parse command line keys."""
4451    parser = ArgumentParser()  # command-line string parser
4452
4453    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4454    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4455
4456    # --- options:
4457
4458    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4459    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4460    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4461
4462    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4463    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4464
4465    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4466    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4467
4468    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4469
4470    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4471    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4472    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4473
4474    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4475    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4476
4477    # --- commands:
4478
4479    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4480
4481    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4482    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4483    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4484    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4485    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4486    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4487    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4488    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4489
4490    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4491    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4492    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4493    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4494    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4495    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4496
4497    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4498    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4499    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4500    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4501
4502    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4503    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4504    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4505
4506    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4507    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4508    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4509    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4510    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4511    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4512    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4513
4514    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4515    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4516    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4517    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4518    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4519
4520    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4521    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4522    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4523
4524    cmdArgs = parser.parse_args()
4525    return cmdArgs
4526
4527
4528def Main(**kwargs):
4529    """
4530    Main function for work with TKSBrokerAPI in the console.
4531
4532    See examples:
4533    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4534    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4535    """
4536    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4537
4538    if args.debug_level:
4539        uLogger.level = 10  # always debug level by default
4540        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4541
4542    exitCode = 0
4543    start = datetime.now(tzutc())
4544    uLogger.debug("=-" * 50)
4545    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4546        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4547        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4548    ))
4549
4550    # trying to calculate full current version:
4551    buildVersion = __version__
4552    try:
4553        v = version("tksbrokerapi")
4554        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4555
4556    except Exception:
4557        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4558
4559    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4560    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4561
4562    try:
4563        if args.version:
4564            print("TKSBrokerAPI {}".format(buildVersion))
4565            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4566
4567        else:
4568            # Init class for trading with Tinkoff Broker:
4569            trader = TinkoffBrokerServer(
4570                token=args.token,
4571                accountId=args.account_id,
4572                useCache=not args.no_cache,
4573            )
4574
4575            # --- set some options:
4576
4577            if args.more:
4578                trader.moreDebug = True
4579                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4580
4581            if args.ticker:
4582                ticker = args.ticker.upper()  # Tickers may be upper case only
4583
4584                if ticker in trader.aliasesKeys:
4585                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4586
4587                else:
4588                    trader.ticker = ticker
4589
4590            if args.figi:
4591                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4592
4593            if args.depth is not None:
4594                trader.depth = args.depth
4595
4596            # --- do one command:
4597
4598            if args.list:
4599                if args.output is not None:
4600                    trader.instrumentsFile = args.output
4601
4602                trader.ShowInstrumentsInfo(show=True)
4603
4604            elif args.list_xlsx:
4605                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4606
4607            elif args.bonds_xlsx is not None:
4608                if args.output is not None:
4609                    trader.bondsXLSXFile = args.output
4610
4611                if len(args.bonds_xlsx) == 0:
4612                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4613
4614                else:
4615                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4616
4617            elif args.search:
4618                if args.output is not None:
4619                    trader.searchResultsFile = args.output
4620
4621                trader.SearchInstruments(pattern=args.search[0], show=True)
4622
4623            elif args.info:
4624                if not (args.ticker or args.figi):
4625                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4626                    raise Exception("Ticker or FIGI required")
4627
4628                if args.output is not None:
4629                    trader.infoFile = args.output
4630
4631                if args.ticker:
4632                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4633
4634                else:
4635                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4636
4637            elif args.calendar is not None:
4638                if args.output is not None:
4639                    trader.calendarFile = args.output
4640
4641                if len(args.calendar) == 0:
4642                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4643
4644                else:
4645                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4646
4647                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4648
4649            elif args.price:
4650                if not (args.ticker or args.figi):
4651                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4652                    raise Exception("Ticker or FIGI required")
4653
4654                trader.GetCurrentPrices(show=True)
4655
4656            elif args.prices is not None:
4657                if args.output is not None:
4658                    trader.pricesFile = args.output
4659
4660                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4661
4662            elif args.overview:
4663                if args.output is not None:
4664                    trader.overviewFile = args.output
4665
4666                trader.Overview(show=True, details="full")
4667
4668            elif args.overview_digest:
4669                if args.output is not None:
4670                    trader.overviewDigestFile = args.output
4671
4672                trader.Overview(show=True, details="digest")
4673
4674            elif args.overview_positions:
4675                if args.output is not None:
4676                    trader.overviewPositionsFile = args.output
4677
4678                trader.Overview(show=True, details="positions")
4679
4680            elif args.overview_orders:
4681                if args.output is not None:
4682                    trader.overviewOrdersFile = args.output
4683
4684                trader.Overview(show=True, details="orders")
4685
4686            elif args.overview_analytics:
4687                if args.output is not None:
4688                    trader.overviewAnalyticsFile = args.output
4689
4690                trader.Overview(show=True, details="analytics")
4691
4692            elif args.overview_calendar:
4693                if args.output is not None:
4694                    trader.overviewAnalyticsFile = args.output
4695
4696                trader.Overview(show=True, details="calendar")
4697
4698            elif args.deals is not None:
4699                if args.output is not None:
4700                    trader.reportFile = args.output
4701
4702                if 0 <= len(args.deals) < 3:
4703                    trader.Deals(
4704                        start=args.deals[0] if len(args.deals) >= 1 else None,
4705                        end=args.deals[1] if len(args.deals) == 2 else None,
4706                        show=True,  # Always show deals report in console
4707                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4708                    )
4709
4710                else:
4711                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4712                    raise Exception("Incorrect value")
4713
4714            elif args.history is not None:
4715                if args.output is not None:
4716                    trader.historyFile = args.output
4717
4718                if 0 <= len(args.history) < 3:
4719                    dataReceived = trader.History(
4720                        start=args.history[0] if len(args.history) >= 1 else None,
4721                        end=args.history[1] if len(args.history) == 2 else None,
4722                        interval="hour" if args.interval is None or not args.interval else args.interval,
4723                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4724                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4725                        show=True,  # shows all downloaded candles in console
4726                    )
4727
4728                    if args.render_chart is not None and dataReceived is not None:
4729                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4730
4731                        trader.ShowHistoryChart(
4732                            candles=dataReceived,
4733                            interact=iChart,
4734                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4735                        )
4736
4737                else:
4738                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4739                    raise Exception("Incorrect value")
4740
4741            elif args.load_history is not None:
4742                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4743
4744                if args.render_chart is not None and histData is not None:
4745                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4746                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4747
4748                    trader.ShowHistoryChart(
4749                        candles=histData,
4750                        interact=iChart,
4751                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4752                    )
4753
4754            elif args.trade is not None:
4755                if 1 <= len(args.trade) <= 5:
4756                    trader.Trade(
4757                        operation=args.trade[0],
4758                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4759                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4760                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4761                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4762                    )
4763
4764                else:
4765                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4766
4767            elif args.buy is not None:
4768                if 0 <= len(args.buy) <= 4:
4769                    trader.Buy(
4770                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4771                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4772                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4773                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4774                    )
4775
4776                else:
4777                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4778
4779            elif args.sell is not None:
4780                if 0 <= len(args.sell) <= 4:
4781                    trader.Sell(
4782                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4783                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4784                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4785                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4786                    )
4787
4788                else:
4789                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4790
4791            elif args.order:
4792                if 4 <= len(args.order) <= 7:
4793                    trader.Order(
4794                        operation=args.order[0],
4795                        orderType=args.order[1],
4796                        lots=int(args.order[2]),
4797                        targetPrice=float(args.order[3]),
4798                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4799                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4800                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4801                    )
4802
4803                else:
4804                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4805
4806            elif args.buy_limit:
4807                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4808
4809            elif args.sell_limit:
4810                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4811
4812            elif args.buy_stop:
4813                if 2 <= len(args.buy_stop) <= 7:
4814                    trader.BuyStop(
4815                        lots=int(args.buy_stop[0]),
4816                        targetPrice=float(args.buy_stop[1]),
4817                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4818                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4819                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4820                    )
4821
4822                else:
4823                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4824
4825            elif args.sell_stop:
4826                if 2 <= len(args.sell_stop) <= 7:
4827                    trader.SellStop(
4828                        lots=int(args.sell_stop[0]),
4829                        targetPrice=float(args.sell_stop[1]),
4830                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4831                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4832                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4833                    )
4834
4835                else:
4836                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4837
4838            # elif args.buy_order_grid is not None:
4839            #     # update order grid work with api v2
4840            #     if len(args.buy_order_grid) == 2:
4841            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4842            #
4843            #         for order in orderParams:
4844            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4845            #
4846            #     else:
4847            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4848            #
4849            # elif args.sell_order_grid is not None:
4850            #     # update order grid work with api v2
4851            #     if len(args.sell_order_grid) >= 2:
4852            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4853            #
4854            #         for order in orderParams:
4855            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4856            #
4857            #     else:
4858            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4859
4860            elif args.close_order is not None:
4861                trader.CloseOrders(args.close_order)  # close only one order
4862
4863            elif args.close_orders is not None:
4864                trader.CloseOrders(args.close_orders)  # close list of orders
4865
4866            elif args.close_trade:
4867                if not (args.ticker or args.figi):
4868                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4869                    raise Exception("Ticker or FIGI required")
4870
4871                if args.ticker:
4872                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4873
4874                else:
4875                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4876
4877            elif args.close_trades is not None:
4878                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4879
4880            elif args.close_all is not None:
4881                trader.CloseAll(*args.close_all)
4882
4883            elif args.limits:
4884                if args.output is not None:
4885                    trader.withdrawalLimitsFile = args.output
4886
4887                trader.OverviewLimits(show=True)
4888
4889            elif args.user_info:
4890                if args.output is not None:
4891                    trader.userInfoFile = args.output
4892
4893                trader.OverviewUserInfo(show=True)
4894
4895            elif args.account:
4896                if args.output is not None:
4897                    trader.userAccountsFile = args.output
4898
4899                trader.OverviewAccounts(show=True)
4900
4901            else:
4902                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4903                raise Exception("There is no command to execute")
4904
4905    except Exception:
4906        trace = tb.format_exc()
4907        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4908            if e in trace:
4909                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4910                break
4911
4912        uLogger.debug(trace)
4913        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4914        exitCode = 255  # an error occurred, must be open a ticket for this issue
4915
4916    finally:
4917        finish = datetime.now(tzutc())
4918
4919        if exitCode == 0:
4920            if args.more:
4921                uLogger.debug("All operations were finished success (summary code is 0).")
4922
4923        else:
4924            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4925                os.path.abspath(uLog.defaultLogFile), exitCode,
4926            ))
4927
4928        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4929        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4930            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4931            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4932        ))
4933        uLogger.debug("=-" * 50)
4934
4935        if not kwargs:
4936            sys.exit(exitCode)
4937
4938        else:
4939            return exitCode
4940
4941
4942if __name__ == "__main__":
4943    Main()
def NanoToFloat(units: str, nano: int) -> float:
80def NanoToFloat(units: str, nano: int) -> float:
81    """
82    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
83
84    `NanoToFloat(units="2", nano=500000000) -> 2.5`
85
86    `NanoToFloat(units="0", nano=50000000) -> 0.05`
87
88    :param units: integer string or integer parameter that represents the integer part of number
89    :param nano: integer string or integer parameter that represents the fractional part of number
90    :return: float view of number
91    """
92    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 95def FloatToNano(number: float) -> dict:
 96    """
 97    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 98
 99    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
100
101    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
102
103    :param number: float number
104    :return: nano-type view of number: `{"units": "string", "nano": integer}`
105    """
106    splitByPoint = str(number).split(".")
107    frac = 0
108
109    if len(splitByPoint) > 1:
110        if len(splitByPoint[1]) <= 9:
111            frac = int("{}{}".format(
112                int(splitByPoint[1]),
113                "0" * (9 - len(splitByPoint[1])),
114            ))
115
116    if (number < 0) and (frac > 0):
117        frac = -frac
118
119    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
122def GetDatesAsString(start: str = None, end: str = None) -> tuple:
123    """
124    Create tuple of date and time strings with timezone parsed from user-friendly date.
125
126    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
127
128    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
129    An error exception will occur if input date has incorrect format.
130
131    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
132    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
133    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
134    Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago.
135
136    Also, you can use keywords for start if `end=None`:
137    `today` (from 00:00:00 to the end of current day),
138    `yesterday` (-1 day from 00:00:00 to 23:59:59),
139    `week` (-7 day from 00:00:00 to the end of current day),
140    `month` (-30 day from 00:00:00 to the end of current day),
141    `year` (-365 day from 00:00:00 to the end of current day),
142
143    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
144             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
145             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
146    """
147    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
148    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
149    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
150
151    # time between start and the end of the current day:
152    if start is None or start.lower() == "today":
153        pass
154
155    # from start of the last day to the end of the last day:
156    elif start.lower() == "yesterday":
157        s -= timedelta(days=1)
158        e -= timedelta(days=1)
159
160    # week (-7 day from 00:00:00 to the end of the current day):
161    elif start.lower() == "week":
162        s -= timedelta(days=6)  # +1 current day already taken into account
163
164    # month (-30 day from 00:00:00 to the end of current day):
165    elif start.lower() == "month":
166        s -= timedelta(days=29)  # +1 current day already taken into account
167
168    # year (-365 day from 00:00:00 to the end of current day):
169    elif start.lower() == "year":
170        s -= timedelta(days=364)  # +1 current day already taken into account
171
172    # -N days ago to the end of current day:
173    elif start.startswith('-') and start[1:].isdigit():
174        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
175
176    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
177    else:
178        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
179        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
180
181    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
182    s = s.strftime(TKS_DATE_TIME_FORMAT)
183    e = e.strftime(TKS_DATE_TIME_FORMAT)
184
185    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
186
187    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 — how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 190class TinkoffBrokerServer:
 191    """
 192    This class implements methods to work with Tinkoff broker server.
 193
 194    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 195
 196    About `token`: https://tinkoff.github.io/investAPI/token/
 197    """
 198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 199        """
 200        Main class init.
 201
 202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 205        :param useCache: use default cache file with raw data to use instead of `iList`.
 206                         True by default. Cache is auto-update if new day has come.
 207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 208        :param defaultCache: path to default cache file. `dump.json` by default.
 209        """
 210        if token is None or not token:
 211            try:
 212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 214
 215            except KeyError:
 216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 217                raise Exception("Token required")
 218
 219        else:
 220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 222
 223        if accountId is None or not accountId:
 224            try:
 225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 227
 228            except KeyError:
 229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 230
 231        else:
 232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 234
 235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 237
 238        Latest version: https://pypi.org/project/tksbrokerapi/
 239        """
 240
 241        self.aliases = TKS_TICKER_ALIASES
 242        """Some aliases instead official tickers.
 243
 244        See also: `TKSEnums.TKS_TICKER_ALIASES`
 245        """
 246
 247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 248
 249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 250
 251        self.ticker = ""
 252        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 253
 254        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 255        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 256
 257        See also: `SearchByTicker()`, `SearchInstruments()`.
 258        """
 259
 260        self.figi = ""
 261        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 262
 263        See also: `SearchByFIGI()`, `SearchInstruments()`.
 264        """
 265
 266        self.depth = 1
 267        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 268
 269        See also: `GetCurrentPrices()`.
 270        """
 271
 272        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 273        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 274
 275        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 276        """
 277
 278        uLogger.debug("Broker API server: {}".format(self.server))
 279
 280        self.timeout = 15
 281        """Server operations timeout in seconds. Default: `15`.
 282
 283        See also: `SendAPIRequest()`.
 284        """
 285
 286        self.headers = {
 287            "Content-Type": "application/json",
 288            "accept": "application/json",
 289            "Authorization": "Bearer {}".format(self.token),
 290            "x-app-name": "Tim55667757.TKSBrokerAPI",
 291        }
 292        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 293
 294        See also: `SendAPIRequest()`.
 295        """
 296
 297        self.body = None
 298        """Request body which send to broker server. Default: `None`.
 299
 300        See also: `SendAPIRequest()`.
 301        """
 302
 303        self.moreDebug = False
 304        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 305
 306        self.historyFile = None
 307        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 308
 309        See also: `History()`.
 310        """
 311
 312        self.htmlHistoryFile = "index.html"
 313        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 314
 315        See also: `ShowHistoryChart()`.
 316        """
 317
 318        self.instrumentsFile = "instruments.md"
 319        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 320
 321        See also: `ShowInstrumentsInfo()`.
 322        """
 323
 324        self.searchResultsFile = "search-results.md"
 325        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 326
 327        See also: `SearchInstruments()`.
 328        """
 329
 330        self.pricesFile = "prices.md"
 331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 332
 333        See also: `GetListOfPrices()`.
 334        """
 335
 336        self.infoFile = "info.md"
 337        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 338
 339        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 340        """
 341
 342        self.bondsXLSXFile = "ext-bonds.xlsx"
 343        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 344        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 345
 346        See also: `ExtendBondsData()`.
 347        """
 348
 349        self.calendarFile = "calendar.md"
 350        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 351        
 352        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 353
 354        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 355        """
 356
 357        self.overviewFile = "overview.md"
 358        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 359
 360        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 361        """
 362
 363        self.overviewDigestFile = "overview-digest.md"
 364        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 365
 366        See also: `Overview()` with parameter `details="digest"`.
 367        """
 368
 369        self.overviewPositionsFile = "overview-positions.md"
 370        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 371
 372        See also: `Overview()` with parameter `details="positions"`.
 373        """
 374
 375        self.overviewOrdersFile = "overview-orders.md"
 376        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 377
 378        See also: `Overview()` with parameter `details="orders"`.
 379        """
 380
 381        self.overviewAnalyticsFile = "overview-analytics.md"
 382        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 383
 384        See also: `Overview()` with parameter `details="analytics"`.
 385        """
 386
 387        self.overviewBondsCalendarFile = "overview-calendar.md"
 388        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 389
 390        See also: `Overview()` with parameter `details="calendar"`.
 391        """
 392
 393        self.reportFile = "deals.md"
 394        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 395
 396        See also: `Deals()`.
 397        """
 398
 399        self.withdrawalLimitsFile = "limits.md"
 400        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 401
 402        See also: `OverviewLimits()` and `RequestLimits()`.
 403        """
 404
 405        self.userInfoFile = "user-info.md"
 406        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 407
 408        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 409        """
 410
 411        self.userAccountsFile = "accounts.md"
 412        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 413
 414        See also: `OverviewAccounts()`, `RequestAccounts()`.
 415        """
 416
 417        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 418        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 419
 420        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 421
 422        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 423        """
 424
 425        self.iList = None  # init iList for raw instruments data
 426        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 427        
 428        See also: `Listing()`, `DumpInstruments()`.
 429        """
 430
 431        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 432        if useCache:
 433            if os.path.exists(self.iListDumpFile):
 434                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 435                curTime = datetime.now(tzutc())
 436
 437                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 438                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 439
 440                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 441
 442                else:
 443                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 444
 445                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 446                        os.path.abspath(self.iListDumpFile),
 447                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 448                    ))
 449
 450            else:
 451                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 452                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 453
 454        else:
 455            self.iList = self.Listing()  # request new raw instruments data from broker server
 456            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 457
 458        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 459        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 460
 461        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 462        """
 463
 464    def _ParseJSON(self, rawData="{}") -> dict:
 465        """
 466        Parse JSON from response string.
 467
 468        :param rawData: this is a string with JSON-formatted text.
 469        :return: JSON (dictionary), parsed from server response string.
 470        """
 471        responseJSON = json.loads(rawData) if rawData else {}
 472
 473        if self.moreDebug:
 474            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 475
 476        return responseJSON
 477
 478    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 479        """
 480        Send GET or POST request to broker server and receive JSON object.
 481
 482        self.header: must be defining with dictionary of headers.
 483        self.body: if define then used as request body. None by default.
 484        self.timeout: global request timeout, 15 seconds by default.
 485        :param url: url with REST request.
 486        :param reqType: send "GET" or "POST" request. "GET" by default.
 487        :param retry: how many times retry after first request if an 5xx server errors occurred.
 488        :param pause: sleep time in seconds between retries.
 489        :return: response JSON (dictionary) from broker.
 490        """
 491        if reqType not in ("GET", "POST"):
 492            uLogger.error("You can define request type: 'GET' or 'POST'!")
 493            raise Exception("Incorrect value")
 494
 495        if self.moreDebug:
 496            uLogger.debug("Request parameters:")
 497            uLogger.debug("    - REST API URL: {}".format(url))
 498            uLogger.debug("    - request type: {}".format(reqType))
 499            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 500            uLogger.debug("    - body:\n{}".format(self.body))
 501
 502        # fast hack to avoid all operations with some tickers/FIGI
 503        responseJSON = {}
 504        oK = True
 505        for item in self.exclude:
 506            if item in url:
 507                if self.moreDebug:
 508                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 509
 510                oK = False
 511                break
 512
 513        if oK:
 514            counter = 0
 515            response = None
 516            errMsg = ""
 517
 518            while not response and counter <= retry:
 519                if reqType == "GET":
 520                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 521
 522                if reqType == "POST":
 523                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 524
 525                if self.moreDebug:
 526                    uLogger.debug("Response:")
 527                    uLogger.debug("    - status code: {}".format(response.status_code))
 528                    uLogger.debug("    - reason: {}".format(response.reason))
 529                    uLogger.debug("    - body length: {}".format(len(response.text)))
 530                    uLogger.debug("    - headers:\n{}".format(response.headers))
 531
 532                # Server returns some headers:
 533                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 534                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 535                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 536                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 537                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 538                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 539                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 540                    sleep(rateLimitWait)
 541
 542                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 543                if 400 <= response.status_code < 500:
 544                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 545                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 546                    counter = retry + 1
 547
 548                if 500 <= response.status_code < 600:
 549                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 550                    uLogger.debug("    - not oK, {}".format(errMsg))
 551                    counter += 1
 552
 553                    if counter <= retry:
 554                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 555                        sleep(pause)
 556
 557            responseJSON = self._ParseJSON(rawData=response.text)
 558
 559            if errMsg:
 560                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 561                uLogger.error("    - not oK, {}".format(errMsg))
 562
 563        return responseJSON
 564
 565    def _IUpdater(self, iType: str) -> tuple:
 566        """
 567        Request instrument by type from server. See available API methods for instruments:
 568        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 569        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 570        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 571        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 572        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 573
 574        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 575        :return: tuple with iType name and list of available instruments of current type for defined user token.
 576        """
 577        result = []
 578
 579        if iType in TKS_INSTRUMENTS:
 580            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 581
 582            # all instruments have the same body in API v2 requests:
 583            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 584            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 585            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 586
 587        return iType, result
 588
 589    def _IWrapper(self, kwargs):
 590        """
 591        Wrapper runs instrument's update method `_IUpdater()`.
 592        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 593        """
 594        return self._IUpdater(**kwargs)
 595
 596    def Listing(self) -> dict:
 597        """
 598        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 599
 600        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 601        """
 602        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 603        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 604
 605        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 606        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 607        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 608
 609        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 610        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 611        poolUpdater.close()
 612
 613        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 614        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 615        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 616
 617        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 618        for iType in iList.keys():
 619            for ticker in iList[iType]:
 620                iList[iType][ticker]["type"] = iType
 621
 622                if "minPriceIncrement" in iList[iType][ticker].keys():
 623                    iList[iType][ticker]["step"] = NanoToFloat(
 624                        iList[iType][ticker]["minPriceIncrement"]["units"],
 625                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 626                    )
 627
 628                else:
 629                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 630
 631        return iList
 632
 633    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 634        """
 635        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 636
 637        See also: `DumpInstruments()`, `Listing()`.
 638
 639        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 640                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 641        """
 642        if self.iListDumpFile is None or not self.iListDumpFile:
 643            uLogger.error("Output name of dump file must be defined!")
 644            raise Exception("Filename required")
 645
 646        if not self.iList or forceUpdate:
 647            self.iList = self.Listing()
 648
 649        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 650
 651        # Save as XLSX with separated sheets for every type of instruments:
 652        with pd.ExcelWriter(
 653                path=xlsxDumpFile,
 654                date_format=TKS_DATE_FORMAT,
 655                datetime_format=TKS_DATE_TIME_FORMAT,
 656                mode="w",
 657        ) as writer:
 658            for iType in TKS_INSTRUMENTS:
 659                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 660                df = df[sorted(df)]  # sorted by column names
 661                df = df.applymap(
 662                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 663                    na_action="ignore",
 664                )  # converting numbers from nano-type to float in every cell
 665                df.to_excel(
 666                    writer,
 667                    sheet_name=iType,
 668                    encoding="UTF-8",
 669                    freeze_panes=(1, 1),
 670                )  # saving as XLSX-file with freeze first row and column as headers
 671
 672        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 673
 674    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 675        """
 676        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 677        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 678
 679        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 680
 681        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 682                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 683        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 684        """
 685        if self.iListDumpFile is None or not self.iListDumpFile:
 686            uLogger.error("Output name of dump file must be defined!")
 687            raise Exception("Filename required")
 688
 689        if not self.iList or forceUpdate:
 690            self.iList = self.Listing()
 691
 692        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 693        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 694            fH.write(jsonDump)
 695
 696        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 697
 698        return jsonDump
 699
 700    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 701        """
 702        Show information about one instrument defined by json data and prints it in Markdown format.
 703
 704        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 705
 706        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 707        :param show: if `True` then also printing information about instrument and its current price.
 708        :return: multilines text in Markdown format with information about one instrument.
 709        """
 710        splitLine = "|                                                             |                                                        |\n"
 711        infoText = ""
 712
 713        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 714            info = [
 715                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 716                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 717                "| Parameters                                                  | Values                                                 |\n",
 718                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 719                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 720                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 721            ]
 722
 723            if "sector" in iJSON.keys() and iJSON["sector"]:
 724                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 725
 726            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 727                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 728                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 729            )))
 730
 731            info.extend([
 732                splitLine,
 733                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 734                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 735            ])
 736
 737            if "isin" in iJSON.keys() and iJSON["isin"]:
 738                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 739
 740            if "classCode" in iJSON.keys():
 741                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 742
 743            info.extend([
 744                splitLine,
 745                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 746                splitLine,
 747                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 748                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 749                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 750            ])
 751
 752            if iJSON["figi"]:
 753                self.figi = iJSON["figi"]
 754                iJSON = iJSON | self.RequestTradingStatus()
 755
 756                info.extend([
 757                    splitLine,
 758                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 759                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 760                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 761                ])
 762
 763            info.append(splitLine)
 764
 765            if "type" in iJSON.keys() and iJSON["type"]:
 766                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 767
 768            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 769                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 770
 771            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 772                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 773
 774            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 775                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 776
 777            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 778                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 779
 780            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 781                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 782
 783            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 784                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 785
 786            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 787                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 788
 789            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 790                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 791
 792            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 793                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 794
 795            if "currency" in iJSON.keys():
 796                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 797
 798            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 799                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 800
 801            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 802                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 805                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 808                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 809
 810            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 811                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 812
 813            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 814                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 815
 816            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 817                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 818
 819            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 820                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 821
 822            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 823                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 824
 825            iExt = None
 826            if iJSON["type"] == "Bonds":
 827                info.extend([
 828                    splitLine,
 829                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 830                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 831                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 832                        iJSON["nominal"]["currency"],
 833                    )),
 834                ])
 835
 836                if "floatingCouponFlag" in iJSON.keys():
 837                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 838
 839                if "amortizationFlag" in iJSON.keys():
 840                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 841
 842                info.append(splitLine)
 843
 844                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 845                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 846
 847                if iJSON["figi"]:
 848                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 849
 850                    info.extend([
 851                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 852                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 853                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 854                    ])
 855
 856                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 857                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 858                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 859                        iJSON["aciValue"]["currency"]
 860                    )))
 861
 862            if "currentPrice" in iJSON.keys():
 863                info.append(splitLine)
 864
 865                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 866                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 867
 868                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 869                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 870                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 871                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 872                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 873
 874                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 875                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 876
 877                info.extend([
 878                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 879                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 880                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 881                    )),
 882                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 883                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 884                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 885                    )),
 886                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 887                        "{:.2f}%{}".format(
 888                            iJSON["currentPrice"]["changes"],
 889                            " ({}{:.2f} {})".format(
 890                                "+" if bondChangesDelta > 0 else "",
 891                                bondChangesDelta,
 892                                aciCurrency
 893                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 894                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 895                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 896                                currency
 897                            ),
 898                        )
 899                    ),
 900                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 901                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 903                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 904                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 905                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 906                    )),
 907                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 908                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 909                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 910                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 911                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 912                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 913                    )),
 914                ])
 915
 916            if "lot" in iJSON.keys():
 917                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 918
 919            if "step" in iJSON.keys() and iJSON["step"] != 0:
 920                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 921
 922            # Add bond payment calendar:
 923            if iJSON["type"] == "Bonds":
 924                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 925                info.extend(["\n", strCalendar])
 926
 927            infoText += "".join(info)
 928
 929            if show:
 930                uLogger.info("{}".format(infoText))
 931
 932            else:
 933                uLogger.debug("{}".format(infoText))
 934
 935            if self.infoFile is not None:
 936                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 937                    fH.write(infoText)
 938
 939                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 940
 941        return infoText
 942
 943    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 944        """
 945        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 946
 947        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 948        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 949        :return: JSON formatted data with information about instrument.
 950        """
 951        tickerJSON = {}
 952        if self.moreDebug:
 953            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 954
 955        if not self.ticker:
 956            uLogger.warning("self.ticker variable is not be empty!")
 957
 958        else:
 959            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 960                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 961                raise Exception("Instrument not allowed")
 962
 963            if not self.iList:
 964                self.iList = self.Listing()
 965
 966            if self.ticker in self.iList["Shares"].keys():
 967                tickerJSON = self.iList["Shares"][self.ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Currencies"].keys():
 972                tickerJSON = self.iList["Currencies"][self.ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Bonds"].keys():
 977                tickerJSON = self.iList["Bonds"][self.ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 980
 981            elif self.ticker in self.iList["Etfs"].keys():
 982                tickerJSON = self.iList["Etfs"][self.ticker]
 983                if self.moreDebug:
 984                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 985
 986            elif self.ticker in self.iList["Futures"].keys():
 987                tickerJSON = self.iList["Futures"][self.ticker]
 988                if self.moreDebug:
 989                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 990
 991        if tickerJSON:
 992            self.figi = tickerJSON["figi"]
 993
 994            if requestPrice:
 995                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 996
 997                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 998                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 999
1000                else:
1001                    tickerJSON["currentPrice"]["changes"] = 0
1002
1003            if show:
1004                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1005
1006        else:
1007            if show:
1008                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1009
1010        return tickerJSON
1011
1012    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1013        """
1014        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1015
1016        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1017        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1018        :return: JSON formatted data with information about instrument.
1019        """
1020        figiJSON = {}
1021        if self.moreDebug:
1022            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1023
1024        if not self.figi:
1025            uLogger.warning("self.figi variable is not be empty!")
1026
1027        else:
1028            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1029                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1030                raise Exception("Instrument not allowed")
1031
1032            if not self.iList:
1033                self.iList = self.Listing()
1034
1035            for item in self.iList["Shares"].keys():
1036                if self.figi == self.iList["Shares"][item]["figi"]:
1037                    figiJSON = self.iList["Shares"][item]
1038
1039                    if self.moreDebug:
1040                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1041
1042                    break
1043
1044            if not figiJSON:
1045                for item in self.iList["Currencies"].keys():
1046                    if self.figi == self.iList["Currencies"][item]["figi"]:
1047                        figiJSON = self.iList["Currencies"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Bonds"].keys():
1056                    if self.figi == self.iList["Bonds"][item]["figi"]:
1057                        figiJSON = self.iList["Bonds"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Etfs"].keys():
1066                    if self.figi == self.iList["Etfs"][item]["figi"]:
1067                        figiJSON = self.iList["Etfs"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1071
1072                        break
1073
1074            if not figiJSON:
1075                for item in self.iList["Futures"].keys():
1076                    if self.figi == self.iList["Futures"][item]["figi"]:
1077                        figiJSON = self.iList["Futures"][item]
1078
1079                        if self.moreDebug:
1080                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1081
1082                        break
1083
1084        if figiJSON:
1085            self.figi = figiJSON["figi"]
1086            self.ticker = figiJSON["ticker"]
1087
1088            if requestPrice:
1089                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1090
1091                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1092                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1093
1094                else:
1095                    figiJSON["currentPrice"]["changes"] = 0
1096
1097            if show:
1098                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1099
1100        else:
1101            if show:
1102                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1103
1104        return figiJSON
1105
1106    def GetCurrentPrices(self, show: bool = True) -> dict:
1107        """
1108        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1109        `{"buy": [{"price": 1243.8, "quantity": 193},
1110                  {"price": 1244.0, "quantity": 168},
1111                  {"price": 1244.8, "quantity": 5},
1112                  {"price": 1245.0, "quantity": 61},
1113                  {"price": 1245.4, "quantity": 60}],
1114          "sell": [{"price": 1243.6, "quantity": 8},
1115                   {"price": 1242.6, "quantity": 10},
1116                   {"price": 1242.4, "quantity": 18},
1117                   {"price": 1242.2, "quantity": 50},
1118                   {"price": 1242.0, "quantity": 113}],
1119          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1120        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1121        - sell: list of dicts with Buyers prices,
1122            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1123            - quantity: volume value by current price in lots,
1124        - limitUp: current trade session limit price, maximum,
1125        - limitDown: current trade session limit price, minimum,
1126        - lastPrice: last deal price of the instrument,
1127        - closePrice: previous trade session close price of the instrument.
1128
1129        See also: `SearchByTicker()` and `SearchByFIGI()`.
1130        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1131        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1132
1133        :param show: if `True` then print DOM to log and console.
1134        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1135                 If an error occurred then returns an empty record:
1136                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1137        """
1138        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1139
1140        if self.depth < 1:
1141            uLogger.error("Depth of Market (DOM) must be >=1!")
1142            raise Exception("Incorrect value")
1143
1144        if not (self.ticker or self.figi):
1145            uLogger.error("self.ticker or self.figi variables must be defined!")
1146            raise Exception("Ticker or FIGI required")
1147
1148        if self.ticker and not self.figi:
1149            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1150            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1151
1152        if not self.ticker and self.figi:
1153            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1154            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1155
1156        if not self.figi:
1157            uLogger.error("FIGI is not defined!")
1158            raise Exception("Ticker or FIGI required")
1159
1160        else:
1161            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1162
1163            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1164            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1165            self.body = str({"figi": self.figi, "depth": self.depth})
1166            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1167
1168            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1169                # list of dicts with sellers orders:
1170                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1171
1172                # list of dicts with buyers orders:
1173                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1174
1175                # max price of instrument at this time:
1176                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1177
1178                # min price of instrument at this time:
1179                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1180
1181                # last price of deal with instrument:
1182                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1183
1184                # last close price of instrument:
1185                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1186
1187            else:
1188                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1189                uLogger.debug("Server response: {}".format(pricesResponse))
1190
1191            if show:
1192                if prices["buy"] or prices["sell"]:
1193                    info = [
1194                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1195                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1196                            self.ticker,
1197                            self.figi,
1198                            self.depth,
1199                        ),
1200                        "-" * 60, "\n",
1201                        "             Orders of Buyers | Orders of Sellers\n",
1202                        "-" * 60, "\n",
1203                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1204                        "-" * 60, "\n",
1205                    ]
1206
1207                    if not prices["buy"]:
1208                        info.append("                              | No orders!\n")
1209                        sumBuy = 0
1210
1211                    else:
1212                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1213                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1214                        for item in maxMinSorted:
1215                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1216
1217                    if not prices["sell"]:
1218                        info.append("No orders!                    |\n")
1219                        sumSell = 0
1220
1221                    else:
1222                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1223                        for item in prices["sell"]:
1224                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1225
1226                    info.extend([
1227                        "-" * 60, "\n",
1228                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1229                        "-" * 60, "\n",
1230                    ])
1231
1232                    infoText = "".join(info)
1233
1234                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1235
1236                else:
1237                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1238
1239        return prices
1240
1241    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1242        """
1243        This method get and show information about all available broker instruments for current user account.
1244        If `instrumentsFile` string is not empty then also save information to this file.
1245
1246        :param show: if `True` then print results to console, if `False` — print only to file.
1247        :return: multi-lines string with all available broker instruments
1248        """
1249        if not self.iList:
1250            self.iList = self.Listing()
1251
1252        info = [
1253            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1254            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1255        ]
1256
1257        # add instruments count by type:
1258        for iType in self.iList.keys():
1259            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1260
1261        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1262        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1263
1264        # generating info tables with all instruments by type:
1265        for iType in self.iList.keys():
1266            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1267
1268            for instrument in self.iList[iType].keys():
1269                iName = self.iList[iType][instrument]["name"]  # instrument's name
1270                if len(iName) > 57:
1271                    iName = "{}...".format(iName[:54])  # right trim for a long string
1272
1273                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1274                    self.iList[iType][instrument]["ticker"],
1275                    iName,
1276                    self.iList[iType][instrument]["figi"],
1277                    self.iList[iType][instrument]["currency"],
1278                    self.iList[iType][instrument]["lot"],
1279                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1280                ))
1281
1282        infoText = "".join(info)
1283
1284        if show:
1285            uLogger.info(infoText)
1286
1287        if self.instrumentsFile:
1288            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1289                fH.write(infoText)
1290
1291            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1292
1293        return infoText
1294
1295    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1296        """
1297        This method search and show information about instruments by part of its ticker, FIGI or name.
1298        If `searchResultsFile` string is not empty then also save information to this file.
1299
1300        :param pattern: string with part of ticker, FIGI or instrument's name.
1301        :param show: if `True` then print results to console, if `False` — return list of result only.
1302        :return: list of dictionaries with all found instruments.
1303        """
1304        if not self.iList:
1305            self.iList = self.Listing()
1306
1307        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1308        compiledPattern = re.compile(pattern, re.IGNORECASE)
1309
1310        for iType in self.iList:
1311            for instrument in self.iList[iType].values():
1312                searchResult = compiledPattern.search(" ".join(
1313                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1314                ))
1315
1316                if searchResult:
1317                    searchResults[iType][instrument["ticker"]] = instrument
1318
1319        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1320        info = [
1321            "# Search results\n\n",
1322            "* **Search pattern:** [{}]\n".format(pattern),
1323            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1324            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1325        ]
1326        infoShort = info[:]
1327
1328        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1329        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1330        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1331
1332        if resultsLen == 0:
1333            info.append("\nNo results\n")
1334            infoShort.append("\nNo results\n")
1335            uLogger.warning("No results. Try changing your search pattern.")
1336
1337        else:
1338            for iType in searchResults:
1339                iTypeValuesCount = len(searchResults[iType].values())
1340                if iTypeValuesCount > 0:
1341                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1343
1344                    for instrument in searchResults[iType].values():
1345                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1346                            instrument["type"],
1347                            instrument["ticker"],
1348                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1349                            instrument["figi"],
1350                        ))
1351
1352                    if iTypeValuesCount <= 5:
1353                        infoShort.extend(info[-iTypeValuesCount:])
1354
1355                    else:
1356                        infoShort.extend(info[-5:])
1357                        infoShort.append(skippedLine)
1358
1359        infoText = "".join(info)
1360        infoTextShort = "".join(infoShort)
1361
1362        if show:
1363            uLogger.info(infoTextShort)
1364            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1365
1366        if self.searchResultsFile:
1367            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1368                fH.write(infoText)
1369
1370            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1371
1372        return searchResults
1373
1374    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1375        """
1376        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1377
1378        :param instruments: list of strings with tickers or FIGIs.
1379        :return: list with unique instrument FIGIs only.
1380        """
1381        requestedInstruments = []
1382        for iName in instruments:
1383            if iName not in self.aliases.keys():
1384                if iName not in requestedInstruments:
1385                    requestedInstruments.append(iName)
1386
1387            else:
1388                if iName not in requestedInstruments:
1389                    if self.aliases[iName] not in requestedInstruments:
1390                        requestedInstruments.append(self.aliases[iName])
1391
1392        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1393
1394        onlyUniqueFIGIs = []
1395        for iName in requestedInstruments:
1396            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1397                continue
1398
1399            self.ticker = iName
1400            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1401
1402            if not iData:
1403                self.ticker = ""
1404                self.figi = iName
1405
1406                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1407
1408                if not iData:
1409                    self.figi = ""
1410                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1411
1412            if iData and iData["figi"] not in onlyUniqueFIGIs:
1413                onlyUniqueFIGIs.append(iData["figi"])
1414
1415        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1416
1417        return onlyUniqueFIGIs
1418
1419    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1420        """
1421        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1422
1423        See limits: https://tinkoff.github.io/investAPI/limits/
1424
1425        If `pricesFile` string is not empty then also save information to this file.
1426
1427        :param instruments: list of strings with tickers or FIGIs.
1428        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1429        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1430                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1431        """
1432        if instruments is None or not instruments:
1433            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1434            raise Exception("Ticker or FIGI required")
1435
1436        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1437
1438        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1439
1440        iList = []  # trying to get info and current prices about all unique instruments:
1441        for self.figi in onlyUniqueFIGIs:
1442            iData = self.SearchByFIGI(requestPrice=True)
1443            iList.append(iData)
1444
1445        self.ShowListOfPrices(iList, show)
1446
1447        return iList
1448
1449    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1450        """
1451        Show table contains current prices of given instruments.
1452
1453        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1454                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1455        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1456        :return: multilines text in Markdown format as a table contains current prices.
1457        """
1458        infoText = ""
1459
1460        if show or self.pricesFile:
1461            info = [
1462                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1463                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1464                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1465            ]
1466
1467            for item in iList:
1468                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1469                    item["ticker"],
1470                    item["figi"],
1471                    item["type"],
1472                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1473                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1474                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1475                    "{} / {}".format(
1476                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1477                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1478                    ),
1479                    "{} / {}".format(
1480                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1481                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1482                    ),
1483                    item["currency"],
1484                ))
1485
1486            infoText = "".join(info)
1487
1488            if show:
1489                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1490
1491            if self.pricesFile:
1492                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1493                    fH.write(infoText)
1494
1495                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1496
1497        return infoText
1498
1499    def RequestTradingStatus(self) -> dict:
1500        """
1501        Requesting trading status for the instrument defined by `figi` variable.
1502
1503        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1504
1505        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1506
1507        :return: dictionary with trading status attributes. Response example:
1508                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1509                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1510        """
1511        if self.figi is None or not self.figi:
1512            uLogger.error("Variable `figi` must be defined for using this method!")
1513            raise Exception("FIGI required")
1514
1515        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1516
1517        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1518        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1519        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1520
1521        if self.moreDebug:
1522            uLogger.debug("Records about current trading status successfully received")
1523
1524        return tradingStatus
1525
1526    def RequestPortfolio(self) -> dict:
1527        """
1528        Requesting actual user's portfolio for current `accountId`.
1529
1530        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1531
1532        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1533
1534        :return: dictionary with user's portfolio.
1535        """
1536        if self.accountId is None or not self.accountId:
1537            uLogger.error("Variable `accountId` must be defined for using this method!")
1538            raise Exception("Account ID required")
1539
1540        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1541
1542        self.body = str({"accountId": self.accountId})
1543        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1544        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1545
1546        if self.moreDebug:
1547            uLogger.debug("Records about user's portfolio successfully received")
1548
1549        return rawPortfolio
1550
1551    def RequestPositions(self) -> dict:
1552        """
1553        Requesting open positions by currencies and instruments for current `accountId`.
1554
1555        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1556
1557        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1558
1559        :return: dictionary with open positions by instruments.
1560        """
1561        if self.accountId is None or not self.accountId:
1562            uLogger.error("Variable `accountId` must be defined for using this method!")
1563            raise Exception("Account ID required")
1564
1565        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1566
1567        self.body = str({"accountId": self.accountId})
1568        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1569        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1570
1571        if self.moreDebug:
1572            uLogger.debug("Records about current open positions successfully received")
1573
1574        return rawPositions
1575
1576    def RequestPendingOrders(self) -> list:
1577        """
1578        Requesting current actual pending orders for current `accountId`.
1579
1580        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1581
1582        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1583
1584        :return: list of dictionaries with pending orders.
1585        """
1586        if self.accountId is None or not self.accountId:
1587            uLogger.error("Variable `accountId` must be defined for using this method!")
1588            raise Exception("Account ID required")
1589
1590        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1591
1592        self.body = str({"accountId": self.accountId})
1593        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1594        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1595
1596        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1597
1598        return rawOrders
1599
1600    def RequestStopOrders(self) -> list:
1601        """
1602        Requesting current actual stop orders for current `accountId`.
1603
1604        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1605
1606        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1607
1608        :return: list of dictionaries with stop orders.
1609        """
1610        if self.accountId is None or not self.accountId:
1611            uLogger.error("Variable `accountId` must be defined for using this method!")
1612            raise Exception("Account ID required")
1613
1614        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1615
1616        self.body = str({"accountId": self.accountId})
1617        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1618        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1619
1620        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1621
1622        return rawStopOrders
1623
1624    def Overview(self, show: bool = False, details: str = "full") -> dict:
1625        """
1626        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1627        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1628        and `overviewBondsCalendarFile` are defined then also save information to file.
1629
1630        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1631        many requests about the state of the portfolio, and then, based on the received data, a large number
1632        of calculation and statistics are collected.
1633
1634        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1635        :param details: how detailed should the information be?
1636        - `full` — shows full available information about portfolio status (by default),
1637        - `positions` — shows only open positions,
1638        - `orders` — shows only sections of open limits and stop orders.
1639        - `digest` — show a short digest of the portfolio status,
1640        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1641        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1642        :return: dictionary with client's raw portfolio and some statistics.
1643        """
1644        if self.accountId is None or not self.accountId:
1645            uLogger.error("Variable `accountId` must be defined for using this method!")
1646            raise Exception("Account ID required")
1647
1648        view = {
1649            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1650                "headers": {},  # list of dictionaries, response headers without "positions" section
1651                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1652                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1653                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1654                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1655                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1656                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1657                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1658                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1659                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1660            },
1661            "stat": {  # --- some statistics calculated using "raw" sections:
1662                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1663                "availableRUB": 0.,  # available rubles (without other currencies)
1664                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1665                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1666                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1667                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1668                "sharesCostRUB": 0.,  # costs of all shares in RUB
1669                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1670                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1671                "futuresCostRUB": 0.,  # costs of all futures in RUB
1672                "Currencies": [],  # list of dictionaries of all currencies statistics
1673                "Shares": [],  # list of dictionaries of all shares statistics
1674                "Bonds": [],  # list of dictionaries of all bonds statistics
1675                "Etfs": [],  # list of dictionaries of all etfs statistics
1676                "Futures": [],  # list of dictionaries of all futures statistics
1677                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1678                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1679                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1680                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1681                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1682            },
1683            "analytics": {  # --- some analytics of portfolio:
1684                "distrByAssets": {},  # portfolio distribution by assets
1685                "distrByCompanies": {},  # portfolio distribution by companies
1686                "distrBySectors": {},  # portfolio distribution by sectors
1687                "distrByCurrencies": {},  # portfolio distribution by currencies
1688                "distrByCountries": {},  # portfolio distribution by countries
1689                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1690            }
1691        }
1692
1693        details = details.lower()
1694        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1695        if details not in availableDetails:
1696            details = "full"
1697            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1698
1699        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1700
1701        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1702        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1703        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1704        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1705
1706        # save response headers without "positions" section:
1707        for key in portfolioResponse.keys():
1708            if key != "positions":
1709                view["raw"]["headers"][key] = portfolioResponse[key]
1710
1711            else:
1712                continue
1713
1714        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1715        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1716        for item in portfolioResponse["positions"]:
1717            if item["instrumentType"] == "currency":
1718                self.figi = item["figi"]
1719                curr = self.SearchByFIGI(requestPrice=False)
1720
1721                # current price of currency in RUB:
1722                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1723                    "name": curr["name"],
1724                    "currentPrice": NanoToFloat(
1725                        item["currentPrice"]["units"],
1726                        item["currentPrice"]["nano"]
1727                    ),
1728                }
1729
1730                view["raw"]["Currencies"].append(item)
1731
1732            elif item["instrumentType"] == "share":
1733                view["raw"]["Shares"].append(item)
1734
1735            elif item["instrumentType"] == "bond":
1736                view["raw"]["Bonds"].append(item)
1737
1738            elif item["instrumentType"] == "etf":
1739                view["raw"]["Etfs"].append(item)
1740
1741            elif item["instrumentType"] == "futures":
1742                view["raw"]["Futures"].append(item)
1743
1744            else:
1745                continue
1746
1747        # how many volume of currencies (by ISO currency name) are blocked:
1748        for item in view["raw"]["positions"]["blocked"]:
1749            blocked = NanoToFloat(item["units"], item["nano"])
1750            if blocked > 0:
1751                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1752
1753        # how many volume of instruments (by FIGI) are blocked:
1754        for item in view["raw"]["positions"]["securities"]:
1755            blocked = int(item["blocked"])
1756            if blocked > 0:
1757                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1758
1759        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1760
1761        if "rub" in allBlocked.keys():
1762            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1763
1764        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1765        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1766        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1767        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1768        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1769        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1770        view["stat"]["portfolioCostRUB"] = sum([
1771            view["stat"]["allCurrenciesCostRUB"],
1772            view["stat"]["sharesCostRUB"],
1773            view["stat"]["bondsCostRUB"],
1774            view["stat"]["etfsCostRUB"],
1775            view["stat"]["futuresCostRUB"],
1776        ])
1777
1778        # --- calculating some portfolio statistics:
1779        byComp = {}  # distribution by companies
1780        bySect = {}  # distribution by sectors
1781        byCurr = {}  # distribution by currencies (include RUB)
1782        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1783        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1784
1785        for item in portfolioResponse["positions"]:
1786            self.figi = item["figi"]
1787            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1788
1789            if instrument:
1790                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1791                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1792
1793                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1794                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1795
1796                else:
1797                    blocked = 0
1798
1799                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1800                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1801                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1802                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1803                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1804                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1805                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1806                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1807                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1808                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1809                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1810                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1811
1812                statData = {
1813                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1814                    "ticker": instrument["ticker"],  # ticker by FIGI
1815                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1816                    "volume": volume,  # available volume of instrument
1817                    "lots": lots,  # volume in lots of instrument
1818                    "direction": direction,  # direction of an instrument's position: short or long
1819                    "blocked": blocked,  # blocked volume of currency or instrument
1820                    "currentPrice": curPrice,  # current instrument's price in basic asset
1821                    "average": average,  # current average position price
1822                    "cost": cost,  # current cost of all volume of instrument in basic asset
1823                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1824                    "costRUB": costRUB,  # cost of instrument in ruble
1825                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1826                    "profit": profit,  # expected profit at current moment
1827                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1828                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1829                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1830                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1831                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1832                    "step": instrument["step"],  # minimum price increment
1833                }
1834
1835                # adding distribution by unique countries:
1836                if statData["country"] not in byCountry.keys():
1837                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1838
1839                else:
1840                    byCountry[statData["country"]]["cost"] += costRUB
1841                    byCountry[statData["country"]]["percent"] += percentCostRUB
1842
1843                if item["instrumentType"] != "currency":
1844                    # adding distribution by unique companies:
1845                    if statData["name"]:
1846                        if statData["name"] not in byComp.keys():
1847                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1848
1849                        else:
1850                            byComp[statData["name"]]["cost"] += costRUB
1851                            byComp[statData["name"]]["percent"] += percentCostRUB
1852
1853                    # adding distribution by unique sectors:
1854                    if statData["sector"] not in bySect.keys():
1855                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1856
1857                    else:
1858                        bySect[statData["sector"]]["cost"] += costRUB
1859                        bySect[statData["sector"]]["percent"] += percentCostRUB
1860
1861                # adding distribution by unique currencies:
1862                if currency not in byCurr.keys():
1863                    byCurr[currency] = {
1864                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1865                        "cost": costRUB,
1866                        "percent": percentCostRUB
1867                    }
1868
1869                else:
1870                    byCurr[currency]["cost"] += costRUB
1871                    byCurr[currency]["percent"] += percentCostRUB
1872
1873                # saving statistics for every instrument:
1874                if item["instrumentType"] == "currency":
1875                    view["stat"]["Currencies"].append(statData)
1876
1877                    # update dict with free funds for trading (total - blocked) by currencies
1878                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1879                    view["stat"]["funds"][currency] = {
1880                        "total": volume,
1881                        "totalCostRUB": costRUB,  # total volume cost in rubles
1882                        "free": volume - blocked,
1883                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1884                    }
1885
1886                elif item["instrumentType"] == "share":
1887                    view["stat"]["Shares"].append(statData)
1888
1889                elif item["instrumentType"] == "bond":
1890                    view["stat"]["Bonds"].append(statData)
1891
1892                elif item["instrumentType"] == "etf":
1893                    view["stat"]["Etfs"].append(statData)
1894
1895                elif item["instrumentType"] == "Futures":
1896                    view["stat"]["Futures"].append(statData)
1897
1898                else:
1899                    continue
1900
1901        # total changes in Russian Ruble:
1902        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1903        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1904        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1905        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1906        view["stat"]["funds"]["rub"] = {
1907            "total": view["stat"]["availableRUB"],
1908            "totalCostRUB": view["stat"]["availableRUB"],
1909            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1911        }
1912
1913        # --- pending orders sector data:
1914        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1915        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1916
1917        for item in view["raw"]["orders"]:
1918            self.figi = item["figi"]
1919
1920            if item["figi"] not in uniquePendingOrdersFIGIs:
1921                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1922
1923                uniquePendingOrdersFIGIs.append(item["figi"])
1924                uniquePendingOrders[item["figi"]] = instrument
1925
1926            else:
1927                instrument = uniquePendingOrders[item["figi"]]
1928
1929            if instrument:
1930                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_ORDER_TYPES[item["orderType"]]
1932                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1933                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1936                if item["direction"] == "ORDER_DIRECTION_BUY":
1937                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1938
1939                else:
1940                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1941
1942                # requested price for order execution:
1943                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1944
1945                # necessary changes in percent to reach target from current price:
1946                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1947
1948                view["stat"]["orders"].append({
1949                    "orderID": item["orderId"],  # orderId number parameter of current order
1950                    "figi": item["figi"],  # FIGI identification
1951                    "ticker": instrument["ticker"],  # ticker name by FIGI
1952                    "lotsRequested": item["lotsRequested"],  # requested lots value
1953                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1954                    "currentPrice": lastPrice,  # current instrument's price for defined action
1955                    "targetPrice": target,  # requested price for order execution in base currency
1956                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1957                    "percentChanges": changes,  # changes in percent to target from current price
1958                    "currency": item["currency"],  # instrument's currency name
1959                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1960                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1961                    "status": orderState,  # order status from TKS_ORDER_STATES
1962                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1963                })
1964
1965        # --- stop orders sector data:
1966        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1967        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1968
1969        for item in view["raw"]["stopOrders"]:
1970            self.figi = item["figi"]
1971
1972            if item["figi"] not in uniqueStopOrdersFIGIs:
1973                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1974
1975                uniqueStopOrdersFIGIs.append(item["figi"])
1976                uniqueStopOrders[item["figi"]] = instrument
1977
1978            else:
1979                instrument = uniqueStopOrders[item["figi"]]
1980
1981            if instrument:
1982                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1983                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1984                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1985
1986                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1987                if "expirationTime" in item.keys():
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1989                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1990
1991                else:
1992                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1993                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1994
1995                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1996                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1997                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1998
1999                else:
2000                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2001
2002                # requested price when stop-order executed:
2003                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2004
2005                # price for limit-order, set up when stop-order executed:
2006                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2007
2008                # necessary changes in percent to reach target from current price:
2009                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2010
2011                view["stat"]["stopOrders"].append({
2012                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2013                    "figi": item["figi"],  # FIGI identification
2014                    "ticker": instrument["ticker"],  # ticker name by FIGI
2015                    "lotsRequested": item["lotsRequested"],  # requested lots value
2016                    "currentPrice": lastPrice,  # current instrument's price for defined action
2017                    "targetPrice": target,  # requested price for stop-order execution in base currency
2018                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2019                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2020                    "percentChanges": changes,  # changes in percent to target from current price
2021                    "currency": item["currency"],  # instrument's currency name
2022                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2023                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2024                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2025                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2026                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2027                })
2028
2029        # --- calculating data for analytics section:
2030        # portfolio distribution by assets:
2031        view["analytics"]["distrByAssets"] = {
2032            "Ruble": {
2033                "uniques": 1,
2034                "cost": view["stat"]["availableRUB"],
2035                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2036            },
2037            "Currencies": {
2038                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2039                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2040                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041            },
2042            "Shares": {
2043                "uniques": len(view["stat"]["Shares"]),
2044                "cost": view["stat"]["sharesCostRUB"],
2045                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046            },
2047            "Bonds": {
2048                "uniques": len(view["stat"]["Bonds"]),
2049                "cost": view["stat"]["bondsCostRUB"],
2050                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2051            },
2052            "Etfs": {
2053                "uniques": len(view["stat"]["Etfs"]),
2054                "cost": view["stat"]["etfsCostRUB"],
2055                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2056            },
2057            "Futures": {
2058                "uniques": len(view["stat"]["Futures"]),
2059                "cost": view["stat"]["futuresCostRUB"],
2060                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2061            },
2062        }
2063
2064        # portfolio distribution by companies:
2065        view["analytics"]["distrByCompanies"]["All money cash"] = {
2066            "ticker": "",
2067            "cost": view["stat"]["allCurrenciesCostRUB"],
2068            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069        }
2070        view["analytics"]["distrByCompanies"].update(byComp)
2071
2072        # portfolio distribution by sectors:
2073        view["analytics"]["distrBySectors"]["All money cash"] = {
2074            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2075            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2076        }
2077        view["analytics"]["distrBySectors"].update(bySect)
2078
2079        # portfolio distribution by currencies:
2080        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2081            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCurrencies"].update(byCurr)
2087        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # portfolio distribution by countries:
2091        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2092            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2093
2094            if self.moreDebug:
2095                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2096
2097        view["analytics"]["distrByCountries"].update(byCountry)
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2099        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2100
2101        # --- Prepare text statistics overview in human-readable:
2102        if show:
2103            # Whatever the value `details`, header not changes:
2104            info = [
2105                "# Client's portfolio\n\n",
2106                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2107                "* **Account ID:** [{}]\n".format(self.accountId),
2108            ]
2109
2110            if details in ["full", "positions", "digest"]:
2111                info.extend([
2112                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2113                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2114                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2115                        view["stat"]["totalChangesRUB"],
2116                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2117                        view["stat"]["totalChangesPercentRUB"],
2118                    ),
2119                ])
2120
2121            if details in ["full", "positions"]:
2122                info.extend([
2123                    "## Open positions\n\n",
2124                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2125                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2126                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2127                        "{:.2f} ({:.2f}) rub".format(
2128                            view["stat"]["availableRUB"],
2129                            view["stat"]["blockedRUB"],
2130                        )
2131                    )
2132                ])
2133
2134                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2135                    return [
2136                        "|                             |                                 |          |              |              |                     |                              |\n",
2137                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2138                            noTradeStr if noTradeStr else typeStr,
2139                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2140                        ),
2141                    ]
2142
2143                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2144                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2145                        "{} [{}]".format(data["ticker"], data["figi"]),
2146                        "{:.2f} ({:.2f}) {}".format(
2147                            data["volume"],
2148                            data["blocked"],
2149                            data["currency"],
2150                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2151                            data["volume"],
2152                            data["blocked"],
2153                        ),
2154                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2155                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2156                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2157                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2158                        "{}{:.2f} {} ({}{:.2f}%)".format(
2159                            "+" if data["profit"] > 0 else "",
2160                            data["profit"], data["baseCurrencyName"],
2161                            "+" if data["percentProfit"] > 0 else "",
2162                            data["percentProfit"],
2163                        ),
2164                    )
2165
2166                # --- Show currencies section:
2167                if view["stat"]["Currencies"]:
2168                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2169                    for item in view["stat"]["Currencies"]:
2170                        info.append(_InfoStr(item, showCurrencyName=True))
2171
2172                else:
2173                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2174
2175                # --- Show shares section:
2176                if view["stat"]["Shares"]:
2177                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2178
2179                    for item in view["stat"]["Shares"]:
2180                        info.append(_InfoStr(item))
2181
2182                else:
2183                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2184
2185                # --- Show bonds section:
2186                if view["stat"]["Bonds"]:
2187                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2188
2189                    for item in view["stat"]["Bonds"]:
2190                        info.append(_InfoStr(item))
2191
2192                else:
2193                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2194
2195                # --- Show etfs section:
2196                if view["stat"]["Etfs"]:
2197                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2198
2199                    for item in view["stat"]["Etfs"]:
2200                        info.append(_InfoStr(item))
2201
2202                else:
2203                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2204
2205                # --- Show futures section:
2206                if view["stat"]["Futures"]:
2207                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2208
2209                    for item in view["stat"]["Futures"]:
2210                        info.append(_InfoStr(item))
2211
2212                else:
2213                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2214
2215            if details in ["full", "orders"]:
2216                # --- Show pending orders section:
2217                if view["stat"]["orders"]:
2218                    info.extend([
2219                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2220                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2221                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2222                    ])
2223
2224                    for item in view["stat"]["orders"]:
2225                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2226                            "{} [{}]".format(item["ticker"], item["figi"]),
2227                            item["orderID"],
2228                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2229                            "{} {} ({}{:.2f}%)".format(
2230                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2231                                item["baseCurrencyName"],
2232                                "+" if item["percentChanges"] > 0 else "",
2233                                float(item["percentChanges"]),
2234                            ),
2235                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2236                            item["action"],
2237                            item["type"],
2238                            item["date"],
2239                        ))
2240
2241                else:
2242                    info.append("\n## Total pending limit-orders: 0\n")
2243
2244                # --- Show stop orders section:
2245                if view["stat"]["stopOrders"]:
2246                    info.extend([
2247                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2248                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2249                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2250                    ])
2251
2252                    for item in view["stat"]["stopOrders"]:
2253                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2254                            "{} [{}]".format(item["ticker"], item["figi"]),
2255                            item["orderID"],
2256                            item["lotsRequested"],
2257                            "{} {} ({}{:.2f}%)".format(
2258                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2259                                item["baseCurrencyName"],
2260                                "+" if item["percentChanges"] > 0 else "",
2261                                float(item["percentChanges"]),
2262                            ),
2263                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2264                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2265                            item["action"],
2266                            item["type"],
2267                            item["expType"],
2268                            item["createDate"],
2269                            item["expDate"],
2270                        ))
2271
2272                else:
2273                    info.append("\n## Total stop-orders: 0\n")
2274
2275            if details in ["full", "analytics"]:
2276                # -- Show analytics section:
2277                if view["stat"]["portfolioCostRUB"] > 0:
2278                    info.extend([
2279                        "\n# Analytics\n"
2280                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2281                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2282                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2283                            view["stat"]["totalChangesRUB"],
2284                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2285                            view["stat"]["totalChangesPercentRUB"],
2286                        ),
2287                        "\n## Portfolio distribution by assets\n"
2288                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2289                        "|------------------------------------|---------|---------|--------------------|\n",
2290                    ])
2291
2292                    for key in view["analytics"]["distrByAssets"].keys():
2293                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2294                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2295                                key,
2296                                view["analytics"]["distrByAssets"][key]["uniques"],
2297                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2299                            ))
2300
2301                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2302
2303                    info.extend([
2304                        "\n## Portfolio distribution by companies\n"
2305                        "\n| Company                                      | Percent | Current cost       |\n",
2306                        aSepLine,
2307                    ])
2308
2309                    for company in view["analytics"]["distrByCompanies"].keys():
2310                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2311                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2312                                "{}{}".format(
2313                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2314                                    company,
2315                                ),
2316                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2317                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2318                            ))
2319
2320                    info.extend([
2321                        "\n## Portfolio distribution by sectors\n"
2322                        "\n| Sector                                       | Percent | Current cost       |\n",
2323                        aSepLine,
2324                    ])
2325
2326                    for sector in view["analytics"]["distrBySectors"].keys():
2327                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2328                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2329                                sector,
2330                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2332                            ))
2333
2334                    info.extend([
2335                        "\n## Portfolio distribution by currencies\n"
2336                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2337                        aSepLine,
2338                    ])
2339
2340                    for curr in view["analytics"]["distrByCurrencies"].keys():
2341                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2342                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2343                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2344                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2345                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2346                            ))
2347
2348                    info.extend([
2349                        "\n## Portfolio distribution by countries\n"
2350                        "\n| Assets by country                            | Percent | Current cost       |\n",
2351                        aSepLine,
2352                    ])
2353
2354                    for country in view["analytics"]["distrByCountries"].keys():
2355                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2356                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2357                                country,
2358                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2359                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2360                            ))
2361
2362            if details in ["full", "calendar"]:
2363                # -- Show bonds payment calendar section:
2364                if view["stat"]["Bonds"]:
2365                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2366                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2367                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2368
2369                else:
2370                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2371
2372            infoText = "".join(info)
2373
2374            uLogger.info(infoText)
2375
2376            if details == "full" and self.overviewFile:
2377                filename = self.overviewFile
2378
2379            elif details == "digest" and self.overviewDigestFile:
2380                filename = self.overviewDigestFile
2381
2382            elif details == "positions" and self.overviewPositionsFile:
2383                filename = self.overviewPositionsFile
2384
2385            elif details == "orders" and self.overviewOrdersFile:
2386                filename = self.overviewOrdersFile
2387
2388            elif details == "analytics" and self.overviewAnalyticsFile:
2389                filename = self.overviewAnalyticsFile
2390
2391            elif details == "calendar" and self.overviewBondsCalendarFile:
2392                filename = self.overviewBondsCalendarFile
2393
2394            else:
2395                filename = ""
2396
2397            if filename:
2398                with open(filename, "w", encoding="UTF-8") as fH:
2399                    fH.write(infoText)
2400
2401                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2402
2403        return view
2404
2405    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2406        """
2407        Returns history operations between two given dates for current `accountId`.
2408        If `reportFile` string is not empty then also save human-readable report.
2409        Shows some statistical data of closed positions.
2410
2411        :param start: see docstring in `GetDatesAsString()` method
2412        :param end: see docstring in `GetDatesAsString()` method
2413        :param show: if `True` then also prints all records to the console.
2414        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2415        :return: original list of dictionaries with history of deals records from API ("operations" key):
2416                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2417                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2418        """
2419        if self.accountId is None or not self.accountId:
2420            uLogger.error("Variable `accountId` must be defined for using this method!")
2421            raise Exception("Account ID required")
2422
2423        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2424
2425        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2426
2427        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2428        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2429        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2430        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2431        customStat = {}  # custom statistics in additional to responseJSON
2432
2433        # --- output report in human-readable format:
2434        if show or self.reportFile:
2435            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2436            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2437            nextDay = ""
2438
2439            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2440
2441            if len(ops) > 0:
2442                customStat = {
2443                    "opsCount": 0,  # total operations count
2444                    "buyCount": 0,  # buy operations
2445                    "sellCount": 0,  # sell operations
2446                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2447                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2448                    "payIn": {"rub": 0.},  # Deposit brokerage account
2449                    "payOut": {"rub": 0.},  # Withdrawals
2450                    "divs": {"rub": 0.},  # Dividends income
2451                    "coupons": {"rub": 0.},  # Coupon's income
2452                    "brokerCom": {"rub": 0.},  # Service commissions
2453                    "serviceCom": {"rub": 0.},  # Service commissions
2454                    "marginCom": {"rub": 0.},  # Margin commissions
2455                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2456                }
2457
2458                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2459                for item in ops:
2460                    if item["state"] == "OPERATION_STATE_EXECUTED":
2461                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2462
2463                        # count buy operations:
2464                        if "_BUY" in item["operationType"]:
2465                            customStat["buyCount"] += 1
2466
2467                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2468                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2472
2473                        # count sell operations:
2474                        elif "_SELL" in item["operationType"]:
2475                            customStat["sellCount"] += 1
2476
2477                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2478                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2479
2480                            else:
2481                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2482
2483                        # count incoming operations:
2484                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2485                            if item["payment"]["currency"] in customStat["payIn"].keys():
2486                                customStat["payIn"][item["payment"]["currency"]] += payment
2487
2488                            else:
2489                                customStat["payIn"][item["payment"]["currency"]] = payment
2490
2491                        # count withdrawals operations:
2492                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2493                            if item["payment"]["currency"] in customStat["payOut"].keys():
2494                                customStat["payOut"][item["payment"]["currency"]] += payment
2495
2496                            else:
2497                                customStat["payOut"][item["payment"]["currency"]] = payment
2498
2499                        # count dividends income:
2500                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2501                            if item["payment"]["currency"] in customStat["divs"].keys():
2502                                customStat["divs"][item["payment"]["currency"]] += payment
2503
2504                            else:
2505                                customStat["divs"][item["payment"]["currency"]] = payment
2506
2507                        # count coupon's income:
2508                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2509                            if item["payment"]["currency"] in customStat["coupons"].keys():
2510                                customStat["coupons"][item["payment"]["currency"]] += payment
2511
2512                            else:
2513                                customStat["coupons"][item["payment"]["currency"]] = payment
2514
2515                        # count broker commissions:
2516                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2517                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2518                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2519
2520                            else:
2521                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2522
2523                        # count service commissions:
2524                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2525                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2526                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2527
2528                            else:
2529                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2530
2531                        # count margin commissions:
2532                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2533                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2534                                customStat["marginCom"][item["payment"]["currency"]] += payment
2535
2536                            else:
2537                                customStat["marginCom"][item["payment"]["currency"]] = payment
2538
2539                        # count withholding taxes:
2540                        elif "_TAX" in item["operationType"]:
2541                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2542                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2543
2544                            else:
2545                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2546
2547                        else:
2548                            continue
2549
2550                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2551
2552                # --- view "Actions" lines:
2553                info.extend([
2554                    "| Report sections            |                               |                              |                      |                        |\n",
2555                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2556                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2557                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2558                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2559                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2560                    ),
2561                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2562                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2563                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2564                    ),
2565                ])
2566
2567                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2568                for key in opsKeys:
2569                    if key == "rub":
2570                        continue
2571
2572                    info.extend([
2573                        "|                            |                               | {:<28} |                      |                        |\n".format(
2574                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2575                        ),
2576                        "|                            |                               | {:<28} |                      |                        |\n".format(
2577                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2578                        ),
2579                    ])
2580
2581                info.append(splitLine1)
2582
2583                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2584                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2585                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2587                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2588                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2589                    )
2590
2591                # --- view "Payments" lines:
2592                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2593                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2594
2595                for key in paymentsKeys:
2596                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2597
2598                info.append(splitLine1)
2599
2600                # --- view "Commissions and taxes" lines:
2601                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2602                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2603
2604                for key in comKeys:
2605                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2606
2607                info.append(splitLine1)
2608
2609                info.extend([
2610                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2611                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2612                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2613                ])
2614
2615            else:
2616                info.append("Broker returned no operations during this period\n")
2617
2618            # --- view "Operations" section:
2619            for item in ops:
2620                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2621                    continue
2622
2623                else:
2624                    self.figi = item["figi"] if item["figi"] else ""
2625                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2626                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2627
2628                    # group of deals during one day:
2629                    if nextDay and item["date"].split("T")[0] != nextDay:
2630                        info.append(splitLine2)
2631                        nextDay = ""
2632
2633                    else:
2634                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2635
2636                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2637                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2638                        self.figi if self.figi else "—",
2639                        instrument["ticker"] if instrument else "—",
2640                        instrument["type"] if instrument else "—",
2641                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2642                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2643                        TKS_OPERATION_STATES[item["state"]],
2644                        TKS_OPERATION_TYPES[item["operationType"]],
2645                    ))
2646
2647            infoText = "".join(info)
2648
2649            if show:
2650                if self.moreDebug:
2651                    uLogger.debug("Records about history of a client's operations successfully received")
2652
2653                uLogger.info(infoText)
2654
2655            if self.reportFile:
2656                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2657                    fH.write(infoText)
2658
2659                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2660
2661        return ops, customStat
2662
2663    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2664        """
2665        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2666
2667        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2668        Warning! Broker server used ISO UTC time by default.
2669
2670        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2671        Also, `historyFile` used to update history with `onlyMissing` parameter.
2672
2673        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2674
2675        :param start: see docstring in `GetDatesAsString()` method.
2676        :param end: see docstring in `GetDatesAsString()` method.
2677        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2678                         `"hour"`, `"day"`. Default: `"hour"`.
2679        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2680                            False by default. Warning! History appends only from last candle to current time
2681                            with always update last candle!
2682        :param csvSep: separator if csv-file is used, `,` by default.
2683        :param show: if `True` then also prints Pandas DataFrame to the console.
2684        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2685                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2686        """
2687        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2688        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2689        history = None  # empty pandas object for history
2690
2691        if interval not in TKS_CANDLE_INTERVALS.keys():
2692            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2693            raise Exception("Incorrect value")
2694
2695        if not (self.ticker or self.figi):
2696            uLogger.error("Ticker or FIGI must be defined!")
2697            raise Exception("Ticker or FIGI required")
2698
2699        if self.ticker and not self.figi:
2700            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2701            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2702
2703        if self.figi and not self.ticker:
2704            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2705            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2706
2707        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2708        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2709        if interval.lower() != "day":
2710            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2711
2712        delta = dtEnd - dtStart  # current UTC time minus last time in file
2713        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2714
2715        # calculate history length in candles:
2716        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2717        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2718            length += 1  # to avoid fraction time
2719
2720        # calculate data blocks count:
2721        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2722
2723        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2724        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2725        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2726        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2727        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2728
2729        tempOld = None  # pandas object for old history, if --only-missing key present
2730        lastTime = None  # datetime object of last old candle in file
2731
2732        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2733            uLogger.debug("--only-missing key present, add only last missing candles...")
2734            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2735
2736            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2737
2738            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2739            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2740            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2741            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2742
2743            # get last datetime object from last string in file or minus 1 delta if file is empty:
2744            if len(tempOld) > 0:
2745                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2746
2747            else:
2748                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2749
2750            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2751
2752        responseJSONs = []  # raw history blocks of data
2753
2754        blockEnd = dtEnd
2755        for item in range(blocks):
2756            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2757            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2758
2759            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2760                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2761            ))
2762
2763            if blockStart == blockEnd:
2764                uLogger.debug("Skipped this zero-length block...")
2765
2766            else:
2767                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2768                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2769                self.body = str({
2770                    "figi": self.figi,
2771                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2772                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2773                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2774                })
2775                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2776
2777                if "code" in responseJSON.keys():
2778                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2779
2780                else:
2781                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2782                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2783
2784                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2785
2786            blockEnd = blockStart
2787
2788        printCount = len(responseJSONs)  # candles to show in console
2789        if responseJSONs:
2790            tempHistory = pd.DataFrame(
2791                data={
2792                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2793                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2794                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2795                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2796                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2797                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2798                    "volume": [int(item["volume"]) for item in responseJSONs],
2799                },
2800                index=range(len(responseJSONs)),
2801                columns=["date", "time", "open", "high", "low", "close", "volume"],
2802            )
2803            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2804            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2805
2806            # append only newest candles to old history if --only-missing key present:
2807            if onlyMissing and tempOld is not None and lastTime is not None:
2808                index = 0  # find start index in tempHistory data:
2809
2810                for i, item in tempHistory.iterrows():
2811                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2812
2813                    if curTime == lastTime:
2814                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2815                        index = i
2816                        printCount = index + 1
2817                        break
2818
2819                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2820
2821            else:
2822                history = tempHistory  # if no `--only-missing` key then load full data from server
2823
2824            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2825
2826        if history is not None and not history.empty:
2827            if show:
2828                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2829                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2830                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2831                ))
2832
2833        else:
2834            uLogger.warning("Received an empty candles history!")
2835
2836        if self.historyFile is not None:
2837            if history is not None and not history.empty:
2838                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2839                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2840
2841            else:
2842                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2843
2844        else:
2845            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2846
2847        return history
2848
2849    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2850        """
2851        Load candles history from csv-file and return Pandas DataFrame object.
2852
2853        See also: `History()` and `ShowHistoryChart()` methods.
2854
2855        :param filePath: path to csv-file to open.
2856        """
2857        loadedHistory = None  # init candles data object
2858
2859        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2860
2861        if os.path.exists(filePath):
2862            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2863
2864            tfStr = self.priceModel.FormattedDelta(
2865                self.priceModel.timeframe,
2866                "{days} days {hours}h {minutes}m {seconds}s",
2867            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2868                self.priceModel.timeframe,
2869                "{hours}h {minutes}m {seconds}s",
2870            )
2871
2872            if loadedHistory is not None and not loadedHistory.empty:
2873                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2874                    len(loadedHistory),
2875                    tfStr,
2876                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2877                )
2878
2879            else:
2880                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2881
2882        else:
2883            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2884
2885        return loadedHistory
2886
2887    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2888        """
2889        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2890
2891        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2892        Default: `index.html` (both for interact and non-interact candlesticks chart).
2893
2894        See also: `History()` and `LoadHistory()` methods.
2895
2896        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2897        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2898                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2899                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2900                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2901        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2902                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2903        """
2904        if isinstance(candles, str):
2905            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2906            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2907
2908        elif isinstance(candles, pd.DataFrame):
2909            self.priceModel.prices = candles  # set candles chain from variable
2910            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2911
2912            if "datetime" not in candles.columns:
2913                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2914
2915        else:
2916            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2917            raise Exception("Incorrect value")
2918
2919        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2920
2921        if interact:
2922            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2923
2924            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2925
2926        else:
2927            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2928
2929            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2930
2931        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2932
2933    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2934        """
2935        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2936        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2937
2938        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2939
2940        :param operation: string "Buy" or "Sell".
2941        :param lots: volume, integer count of lots >= 1.
2942        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2943        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2944        :param expDate: string "Undefined" by default or local date in future,
2945                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2946        :return: JSON with response from broker server.
2947        """
2948        if self.accountId is None or not self.accountId:
2949            uLogger.error("Variable `accountId` must be defined for using this method!")
2950            raise Exception("Account ID required")
2951
2952        if operation is None or not operation or operation not in ("Buy", "Sell"):
2953            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2954            raise Exception("Incorrect value")
2955
2956        if lots is None or lots < 1:
2957            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2958            lots = 1
2959
2960        if tp is None or tp < 0:
2961            tp = 0
2962
2963        if sl is None or sl < 0:
2964            sl = 0
2965
2966        if expDate is None or not expDate:
2967            expDate = "Undefined"
2968
2969        if not (self.ticker or self.figi):
2970            uLogger.error("Ticker or FIGI must be defined!")
2971            raise Exception("Ticker or FIGI required")
2972
2973        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2974        self.ticker = instrument["ticker"]
2975        self.figi = instrument["figi"]
2976
2977        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2978
2979        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2980        self.body = str({
2981            "figi": self.figi,
2982            "quantity": str(lots),
2983            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2984            "accountId": str(self.accountId),
2985            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2986        })
2987        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2988
2989        if "orderId" in response.keys():
2990            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2991                operation, response["orderId"],
2992                self.ticker, self.figi, lots,
2993                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2994                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2995                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2996            ))
2997
2998            if tp > 0:
2999                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3000
3001            if sl > 0:
3002                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3003
3004        else:
3005            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3006
3007        return response
3008
3009    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3010        """
3011        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3012        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3013
3014        See also: `Order()` and `Trade()` docstrings.
3015
3016        :param lots: volume, integer count of lots >= 1.
3017        :param tp: float > 0, take profit price of stop-order.
3018        :param sl: float > 0, stop loss price of stop-order.
3019        :param expDate: it's a local date in future.
3020                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3021        :return: JSON with response from broker server.
3022        """
3023        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3024
3025    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3026        """
3027        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3028        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3029
3030        See also: `Order()` and `Trade()` docstrings.
3031
3032        :param lots: volume, integer count of lots >= 1.
3033        :param tp: float > 0, take profit price of stop-order.
3034        :param sl: float > 0, stop loss price of stop-order.
3035        :param expDate: it's a local date in the future.
3036                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3037        :return: JSON with response from broker server.
3038        """
3039        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3040
3041    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3042        """
3043        Close position of given instruments.
3044
3045        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3046        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3047                         This avoids unnecessary downloading data from the server.
3048        """
3049        if instruments is None or not instruments:
3050            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3051            raise Exception("Ticker or FIGI required")
3052
3053        if isinstance(instruments, str):
3054            instruments = [instruments]
3055
3056        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3057        if uniqueInstruments:
3058            if portfolio is None or not portfolio:
3059                portfolio = self.Overview(show=False)
3060
3061            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3062            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3063
3064            for self.figi in uniqueInstruments:
3065                if self.figi not in allOpened:
3066                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3067                    continue
3068
3069                # search open trade info about instrument by ticker:
3070                instrument = {}
3071                for iType in TKS_INSTRUMENTS:
3072                    if instrument:
3073                        break
3074
3075                    for item in portfolio["stat"][iType]:
3076                        if item["figi"] == self.figi:
3077                            instrument = item
3078                            break
3079
3080                if instrument:
3081                    self.ticker = instrument["ticker"]
3082                    self.figi = instrument["figi"]
3083
3084                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3085                        self.ticker,
3086                        self.figi,
3087                        int(instrument["volume"]),
3088                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3089                    ))
3090
3091                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3092
3093                    if tradeLots > 0:
3094                        if instrument["blocked"] > 0:
3095                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3096                                instrument["blocked"],
3097                                self.ticker,
3098                                tradeLots,
3099                            ))
3100
3101                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3102                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3103
3104                    else:
3105                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3106
3107    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3108        """
3109        Close all positions of given instruments with defined type.
3110
3111        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3112        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3113                         This avoids unnecessary downloading data from the server.
3114        """
3115        if iType not in TKS_INSTRUMENTS:
3116            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3117
3118        else:
3119            if portfolio is None or not portfolio:
3120                portfolio = self.Overview(show=False)
3121
3122            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3123            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3124
3125            if tickers and portfolio:
3126                self.CloseTrades(tickers, portfolio)
3127
3128            else:
3129                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3130
3131    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3132        """
3133        Universal method to create market or limit orders with all available parameters for current `accountId`.
3134        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3135
3136        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3137        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3138
3139        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3140        then broker immediately open market order as you can do simple --buy or --sell operations!
3141
3142        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3143        When current price will go up or down to target price value then broker opens a limit order.
3144        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3145
3146        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3147
3148        :param operation: string "Buy" or "Sell".
3149        :param orderType: string "Limit" or "Stop".
3150        :param lots: volume, integer count of lots >= 1.
3151        :param targetPrice: target price > 0. This is open trade price for limit order.
3152        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3153                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3154        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3155                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3156                         Stop loss order always executed by market price.
3157        :param expDate: string "Undefined" by default or local date in future.
3158                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3159                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3160                        A limit order has no expiration date, it lasts until the end of the trading day.
3161        :return: JSON with response from broker server.
3162        """
3163        if self.accountId is None or not self.accountId:
3164            uLogger.error("Variable `accountId` must be defined for using this method!")
3165            raise Exception("Account ID required")
3166
3167        if operation is None or not operation or operation not in ("Buy", "Sell"):
3168            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3169            raise Exception("Incorrect value")
3170
3171        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3172            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3173            raise Exception("Incorrect value")
3174
3175        if lots is None or lots < 1:
3176            uLogger.error("You must define trade volume > 0: integer count of lots!")
3177            raise Exception("Incorrect value")
3178
3179        if targetPrice is None or targetPrice <= 0:
3180            uLogger.error("Target price for limit-order must be greater than 0!")
3181            raise Exception("Incorrect value")
3182
3183        if limitPrice is None or limitPrice <= 0:
3184            limitPrice = targetPrice
3185
3186        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3187            stopType = "Limit"
3188
3189        if expDate is None or not expDate:
3190            expDate = "Undefined"
3191
3192        if not (self.ticker or self.figi):
3193            uLogger.error("Tocker or FIGI must be defined!")
3194            raise Exception("Ticker or FIGI required")
3195
3196        response = {}
3197        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3198        self.ticker = instrument["ticker"]
3199        self.figi = instrument["figi"]
3200
3201        if orderType == "Limit":
3202            uLogger.debug(
3203                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3204                    self.ticker, self.figi,
3205                    operation, lots, targetPrice, instrument["currency"],
3206                ))
3207
3208            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3209            self.body = str({
3210                "figi": self.figi,
3211                "quantity": str(lots),
3212                "price": FloatToNano(targetPrice),
3213                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3214                "accountId": str(self.accountId),
3215                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3216            })
3217            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3218
3219            if "orderId" in response.keys():
3220                uLogger.info(
3221                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3222                        response["orderId"],
3223                        self.ticker, self.figi,
3224                        operation, lots, targetPrice, instrument["currency"],
3225                    ))
3226
3227                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3228                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3229                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3230                            targetPrice, instrument["currency"],
3231                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3232                        ))
3233
3234                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3235                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3236                            targetPrice, instrument["currency"],
3237                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3238                        ))
3239
3240            else:
3241                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3242
3243        if orderType == "Stop":
3244            uLogger.debug(
3245                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3246                    self.ticker, self.figi,
3247                    operation, lots,
3248                    targetPrice, instrument["currency"],
3249                    limitPrice, instrument["currency"],
3250                    stopType, expDate,
3251                ))
3252
3253            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3254            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3255            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3256
3257            body = {
3258                "figi": self.figi,
3259                "quantity": str(lots),
3260                "price": FloatToNano(limitPrice),
3261                "stopPrice": FloatToNano(targetPrice),
3262                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3263                "accountId": str(self.accountId),
3264                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3265                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3266            }
3267
3268            if expDateUTC:
3269                body["expireDate"] = expDateUTC
3270
3271            self.body = str(body)
3272            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3273
3274            if "stopOrderId" in response.keys():
3275                uLogger.info(
3276                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3277                        response["stopOrderId"],
3278                        self.ticker, self.figi,
3279                        operation, lots,
3280                        targetPrice, instrument["currency"],
3281                        limitPrice, instrument["currency"],
3282                        TKS_STOP_ORDER_TYPES[stopOrderType],
3283                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3284                    ))
3285
3286                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3287                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3288                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3289                            targetPrice, instrument["currency"],
3290                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3291                        ))
3292
3293                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3294                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3295                            targetPrice, instrument["currency"],
3296                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3297                        ))
3298
3299            else:
3300                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3301
3302        return response
3303
3304    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3305        """
3306        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3307        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3308        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3309        See also: `Order()` docstring.
3310
3311        :param lots: volume, integer count of lots >= 1.
3312        :param targetPrice: target price > 0. This is open trade price for limit order.
3313        :return: JSON with response from broker server.
3314        """
3315        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3316
3317    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3318        """
3319        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3320        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3321        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3322        target price value then broker opens a limit order. See also: `Order()` docstring.
3323
3324        :param lots: volume, integer count of lots >= 1.
3325        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3326        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3327                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3328        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3329                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3330        :param expDate: string "Undefined" by default or local date in future.
3331                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3332                        This date is converting to UTC format for server.
3333        :return: JSON with response from broker server.
3334        """
3335        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3336
3337    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3338        """
3339        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3340        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3341        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3342        See also: `Order()` docstring.
3343
3344        :param lots: volume, integer count of lots >= 1.
3345        :param targetPrice: target price > 0. This is open trade price for limit order.
3346        :return: JSON with response from broker server.
3347        """
3348        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3349
3350    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3351        """
3352        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3353        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3354        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3355        target price value then broker opens a limit order. See also: `Order()` docstring.
3356
3357        :param lots: volume, integer count of lots >= 1.
3358        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3359        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3360                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3361        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3362                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3363        :param expDate: string "Undefined" by default or local date in future.
3364                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3365                        This date is converting to UTC format for server.
3366        :return: JSON with response from broker server.
3367        """
3368        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3369
3370    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3371        """
3372        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3373
3374        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3375        :param allOrdersIDs: pre-received lists of all active pending orders.
3376                             This avoids unnecessary downloading data from the server.
3377        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3378        """
3379        if self.accountId is None or not self.accountId:
3380            uLogger.error("Variable `accountId` must be defined for using this method!")
3381            raise Exception("Account ID required")
3382
3383        if orderIDs:
3384            if allOrdersIDs is None or not allOrdersIDs:
3385                rawOrders = self.RequestPendingOrders()
3386                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3387
3388            if allStopOrdersIDs is None or not allStopOrdersIDs:
3389                rawStopOrders = self.RequestStopOrders()
3390                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3391
3392            for orderID in orderIDs:
3393                idInPendingOrders = orderID in allOrdersIDs
3394                idInStopOrders = orderID in allStopOrdersIDs
3395
3396                if not (idInPendingOrders or idInStopOrders):
3397                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3398                    continue
3399
3400                else:
3401                    if idInPendingOrders:
3402                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3403
3404                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3405                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3406                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3407                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3408
3409                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3410                            if self.moreDebug:
3411                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3412
3413                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3414
3415                        else:
3416                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3417
3418                    elif idInStopOrders:
3419                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3420
3421                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3422                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3423                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3424                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3425
3426                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3427                            if self.moreDebug:
3428                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3429
3430                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3431
3432                        else:
3433                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3434
3435                    else:
3436                        continue
3437
3438    def CloseAllOrders(self) -> None:
3439        """
3440        Gets a list of open pending and stop orders and cancel it all.
3441        """
3442        rawOrders = self.RequestPendingOrders()
3443        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3444        lenOrders = len(allOrdersIDs)
3445
3446        rawStopOrders = self.RequestStopOrders()
3447        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3448        lenSOrders = len(allStopOrdersIDs)
3449
3450        if lenOrders > 0 or lenSOrders > 0:
3451            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3452
3453            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3454
3455        else:
3456            uLogger.info("Orders not found, nothing to cancel.")
3457
3458    def CloseAll(self, *args) -> None:
3459        """
3460        Close all available (not blocked) opened trades and orders.
3461
3462        Also, you can select one or more keywords case-insensitive:
3463        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3464
3465        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3466        """
3467        overview = self.Overview(show=False)  # get all open trades info
3468
3469        if len(args) == 0:
3470            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3471            self.CloseAllOrders()  # close all pending and stop orders
3472
3473            for iType in TKS_INSTRUMENTS:
3474                if iType != "Currencies":
3475                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3476
3477        else:
3478            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3479            lowerArgs = [x.lower() for x in args]
3480
3481            if "orders" in lowerArgs:
3482                self.CloseAllOrders()  # close all pending and stop orders
3483
3484            for iType in TKS_INSTRUMENTS:
3485                if iType.lower() in lowerArgs and iType != "Currencies":
3486                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3487
3488    @staticmethod
3489    def ParseOrderParameters(operation, **inputParameters):
3490        """
3491        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3492
3493        :param operation: string "Buy" or "Sell".
3494        :param inputParameters: this is dict of strings that looks like this
3495               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3496               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3497               "prices" key: one or more prices to open limit-orders
3498               Counts of values in lots and prices lists must be equals!
3499        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3500        """
3501        # TODO: update order grid work with api v2
3502        pass
3503        # uLogger.debug("Input parameters: {}".format(inputParameters))
3504        #
3505        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3506        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3507        #     raise Exception("Incorrect value")
3508        #
3509        # if "l" in inputParameters.keys():
3510        #     inputParameters["lots"] = inputParameters.pop("l")
3511        #
3512        # if "p" in inputParameters.keys():
3513        #     inputParameters["prices"] = inputParameters.pop("p")
3514        #
3515        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3516        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3517        #     raise Exception("Incorrect value")
3518        #
3519        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3520        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3521        #
3522        # if len(lots) != len(prices):
3523        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3524        #     raise Exception("Incorrect value")
3525        #
3526        # uLogger.debug("Extracted parameters for orders:")
3527        # uLogger.debug("lots = {}".format(lots))
3528        # uLogger.debug("prices = {}".format(prices))
3529        #
3530        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3531        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3532        # uLogger.debug("Order parameters: {}".format(result))
3533        #
3534        # return result
3535
3536    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3537        """
3538        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3539
3540        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3541        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3542        """
3543        result = False
3544        msg = "Instrument not defined!"
3545
3546        if portfolio is None or not portfolio:
3547            portfolio = self.Overview(show=False)
3548
3549        if self.ticker:
3550            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3551            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3552
3553            for iType in TKS_INSTRUMENTS:
3554                for instrument in portfolio["stat"][iType]:
3555                    if instrument["ticker"] == self.ticker:
3556                        result = True
3557                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3558                        break
3559
3560        elif self.figi:
3561            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3562            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3563
3564            for iType in TKS_INSTRUMENTS:
3565                for instrument in portfolio["stat"][iType]:
3566                    if instrument["figi"] == self.figi:
3567                        result = True
3568                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3569                        break
3570
3571        else:
3572            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3573
3574        uLogger.debug(msg)
3575
3576        return result
3577
3578    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3579        """
3580        Returns instrument from the user's portfolio if it presents there.
3581        Instrument must be defined by `ticker` (highly priority) or `figi`.
3582
3583        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3584        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3585        """
3586        result = None
3587        msg = "Instrument not defined!"
3588
3589        if portfolio is None or not portfolio:
3590            portfolio = self.Overview(show=False)
3591
3592        if self.ticker:
3593            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3594            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3595
3596            for iType in TKS_INSTRUMENTS:
3597                for instrument in portfolio["stat"][iType]:
3598                    if instrument["ticker"] == self.ticker:
3599                        result = instrument
3600                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3601                        break
3602
3603        elif self.figi:
3604            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3605            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3606
3607            for iType in TKS_INSTRUMENTS:
3608                for instrument in portfolio["stat"][iType]:
3609                    if instrument["figi"] == self.figi:
3610                        result = instrument
3611                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3612                        break
3613
3614        else:
3615            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3616
3617        uLogger.debug(msg)
3618
3619        return result
3620
3621    def RequestLimits(self) -> dict:
3622        """
3623        Method for obtaining the available funds for withdrawal for current `accountId`.
3624
3625        See also:
3626        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3627        - `OverviewLimits()` method
3628
3629        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3630                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3631                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3632                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3633        """
3634        if self.accountId is None or not self.accountId:
3635            uLogger.error("Variable `accountId` must be defined for using this method!")
3636            raise Exception("Account ID required")
3637
3638        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3639
3640        self.body = str({"accountId": self.accountId})
3641        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3642        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3643
3644        if self.moreDebug:
3645            uLogger.debug("Records about available funds for withdrawal successfully received")
3646
3647        return rawLimits
3648
3649    def OverviewLimits(self, show: bool = False) -> dict:
3650        """
3651        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3652
3653        See also: `RequestLimits()`.
3654
3655        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3656        :return: dict with raw parsed data from server and some calculated statistics about it.
3657        """
3658        if self.accountId is None or not self.accountId:
3659            uLogger.error("Variable `accountId` must be defined for using this method!")
3660            raise Exception("Account ID required")
3661
3662        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3663
3664        view = {
3665            "rawLimits": rawLimits,
3666            "limits": {  # parsed data for every currency:
3667                "money": {  # this is an array of portfolio currency positions
3668                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3669                },
3670                "blocked": {  # this is an array of blocked currency
3671                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3672                },
3673                "blockedGuarantee": {  # this is locked money under collateral for futures
3674                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3675                },
3676            },
3677        }
3678
3679        # --- Prepare text table with limits in human-readable format:
3680        if show:
3681            info = [
3682                "# Withdrawal limits\n\n",
3683                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3684                "* **Account ID:** [{}]\n".format(self.accountId),
3685            ]
3686
3687            if view["limits"]["money"]:
3688                info.extend([
3689                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3690                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3691                ])
3692
3693            else:
3694                info.append("\nNo withdrawal limits\n")
3695
3696            for curr in view["limits"]["money"].keys():
3697                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3698                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3699                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3700
3701                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3702                    "[{}]".format(curr),
3703                    "{:.2f}".format(view["limits"]["money"][curr]),
3704                    "{:.2f}".format(availableMoney),
3705                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3706                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3707                )
3708
3709                if curr == "rub":
3710                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3711
3712                else:
3713                    info.append(infoStr)
3714
3715            infoText = "".join(info)
3716
3717            uLogger.info(infoText)
3718
3719            if self.withdrawalLimitsFile:
3720                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3721                    fH.write(infoText)
3722
3723                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3724
3725        return view
3726
3727    def RequestAccounts(self) -> dict:
3728        """
3729        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3730
3731        See also:
3732        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3733        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3734        - `OverviewUserInfo()` method
3735
3736        :return: dict with raw data from server that contains accounts info. Example of dict:
3737                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3738                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3739                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3740                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3741        """
3742        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3743
3744        self.body = str({})
3745        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3746        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3747
3748        if self.moreDebug:
3749            uLogger.debug("Records about available accounts successfully received")
3750
3751        return rawAccounts
3752
3753    def RequestUserInfo(self) -> dict:
3754        """
3755        Method for requesting common user's information.
3756
3757        See also:
3758        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3759        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3760        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3761        - `OverviewUserInfo()` method
3762
3763        :return: dict with raw data from server that contains user's information. Example of dict:
3764                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3765                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3766        """
3767        uLogger.debug("Requesting common user's information. Wait, please...")
3768
3769        self.body = str({})
3770        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3771        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3772
3773        if self.moreDebug:
3774            uLogger.debug("Records about current user successfully received")
3775
3776        return rawUserInfo
3777
3778    def RequestMarginStatus(self, accountId: str = None) -> dict:
3779        """
3780        Method for requesting margin calculation for defined account ID.
3781
3782        See also:
3783        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3784        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3785        - `OverviewUserInfo()` method
3786
3787        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3788        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3789                 Example of responses:
3790                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3791                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3792                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3793                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3794                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3795                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3796        """
3797        if accountId is None or not accountId:
3798            if self.accountId is None or not self.accountId:
3799                uLogger.error("Variable `accountId` must be defined for using this method!")
3800                raise Exception("Account ID required")
3801
3802            else:
3803                accountId = self.accountId  # use `self.accountId` (main ID) by default
3804
3805        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3806
3807        self.body = str({"accountId": accountId})
3808        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3809        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3810
3811        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3812            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3813            rawMargin = {}
3814
3815        else:
3816            if self.moreDebug:
3817                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3818
3819        return rawMargin
3820
3821    def RequestTariffLimits(self) -> dict:
3822        """
3823        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3824
3825        See also:
3826        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3827        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3828        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3829        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3830        - `OverviewUserInfo()` method
3831
3832        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3833                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3834                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3835        """
3836        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3837
3838        self.body = str({})
3839        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3840        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3841
3842        if self.moreDebug:
3843            uLogger.debug("Records with limits of current tariff successfully received")
3844
3845        return rawTariffLimits
3846
3847    def RequestBondCoupons(self, iJSON: dict) -> dict:
3848        """
3849        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3850        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3851        All dates are in UTC timezone.
3852
3853        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3854        Documentation:
3855        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3856        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3857
3858        See also: `ExtendBondsData()`.
3859
3860        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3861                      If raw iJSON is not data of bond then server returns an error [400] with message:
3862                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3863        :return: dictionary with bond payment calendar. Response example
3864                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3865                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3866                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3867                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3868        """
3869        if iJSON["figi"] is None or not iJSON["figi"]:
3870            uLogger.error("FIGI must be defined for using this method!")
3871            raise Exception("FIGI required")
3872
3873        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3874        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3875
3876        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3877            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3878            self.figi,
3879            startDate,
3880            endDate,
3881        ))
3882
3883        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3884        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3885        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3886
3887        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3888            uLogger.warning("Instrument type is not bond!")
3889
3890        else:
3891            if self.moreDebug:
3892                uLogger.debug("Records about bond payment calendar successfully received")
3893
3894        return calendar
3895
3896    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3897        """
3898        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3899        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3900        coupon yields, current yields and some statistics etc.
3901
3902        WARNING! This is too long operation if a lot of bonds requested from broker server.
3903
3904        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3905
3906        :param instruments: list of strings with tickers or FIGIs.
3907        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3908                     for further used by data scientists or stock analytics.
3909        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3910                 In XLSX-file and Pandas DataFrame fields mean:
3911                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3912                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3913        """
3914        if instruments is None or not instruments:
3915            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3916            raise Exception("Ticker or FIGI required")
3917
3918        if isinstance(instruments, str):
3919            instruments = [instruments]
3920
3921        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3922
3923        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3924
3925        iCount = len(uniqueInstruments)
3926        tooLong = iCount >= 20
3927        if tooLong:
3928            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3929
3930        bonds = None
3931        for i, self.figi in enumerate(uniqueInstruments):
3932            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3933
3934            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3935                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3936                rawBond = self.SearchByFIGI(requestPrice=True)
3937
3938                # Widen raw data with UTC current time (iData["actualDateTime"]):
3939                actualDate = datetime.now(tzutc())
3940                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3941
3942                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3943                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3944
3945                # Replace some values with human-readable:
3946                iData["nominalCurrency"] = iData["nominal"]["currency"]
3947                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3948                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3949                iData["aciCurrency"] = iData["aciValue"]["currency"]
3950                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3951                iData["issueSize"] = int(iData["issueSize"])
3952                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3953                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3954                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3955                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3956                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3957                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3958                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3959                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3960                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3961                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3962
3963                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3964                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3965                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3966                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3967                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3968                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3969                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3970                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3971                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3972                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3973                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3974
3975                # Widen raw data with calendar data from `rawCalendar` values:
3976                calendarData = []
3977                if "events" in iData["rawCalendar"].keys():
3978                    for item in iData["rawCalendar"]["events"]:
3979                        calendarData.append({
3980                            "couponDate": item["couponDate"],
3981                            "couponNumber": int(item["couponNumber"]),
3982                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3983                            "payCurrency": item["payOneBond"]["currency"],
3984                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3985                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3986                            "couponStartDate": item["couponStartDate"],
3987                            "couponEndDate": item["couponEndDate"],
3988                            "couponPeriod": item["couponPeriod"],
3989                        })
3990
3991                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3992                    if "maturityDate" not in iData.keys():
3993                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3994
3995                # Widen raw data with Coupon Rate.
3996                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3997                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3998                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3999                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4000
4001                # Widen raw data with Yield to Maturity (YTM) on current date.
4002                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4003                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4004                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4005                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4006                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4007                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4008
4009                iData["calendar"] = calendarData  # adds calendar at the end
4010
4011                # Remove not used data:
4012                iData.pop("uid")
4013                iData.pop("positionUid")
4014                iData.pop("currentPrice")
4015                iData.pop("rawCalendar")
4016
4017                colNames = list(iData.keys())
4018                if bonds is None:
4019                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4020
4021                else:
4022                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4023
4024            else:
4025                uLogger.warning("Instrument is not a bond!")
4026
4027            processed = round(100 * (i + 1) / iCount, 1)
4028            if tooLong and processed % 5 == 0:
4029                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4030
4031            else:
4032                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4033
4034        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4035
4036        # Saving bonds from Pandas DataFrame to XLSX sheet:
4037        if xlsx and self.bondsXLSXFile:
4038            with pd.ExcelWriter(
4039                    path=self.bondsXLSXFile,
4040                    date_format=TKS_DATE_FORMAT,
4041                    datetime_format=TKS_DATE_TIME_FORMAT,
4042                    mode="w",
4043            ) as writer:
4044                bonds.to_excel(
4045                    writer,
4046                    sheet_name="Extended bonds data",
4047                    index=True,
4048                    encoding="UTF-8",
4049                    freeze_panes=(1, 1),
4050                )  # saving as XLSX-file with freeze first row and column as headers
4051
4052            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4053
4054        return bonds
4055
4056    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4057        """
4058        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4059
4060        WARNING! This is too long operation if a lot of bonds requested from broker server.
4061
4062        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4063
4064        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4065                        extended information about bonds: main info, current prices, bond payment calendar,
4066                        coupon yields, current yields and some statistics etc.
4067                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4068        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4069                     for further used by data scientists or stock analytics.
4070        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4071        """
4072        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4073            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4074
4075        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4076
4077        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4078        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4079        calendar = None
4080        for bond in extBonds.iterrows():
4081            for item in bond[1]["calendar"]:
4082                cData = {
4083                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4084                    "couponDate": item["couponDate"],
4085                    "figi": bond[1]["figi"],
4086                    "ticker": bond[1]["ticker"],
4087                    "name": bond[1]["name"],
4088                    "couponNumber": item["couponNumber"],
4089                    "payOneBond": item["payOneBond"],
4090                    "payCurrency": item["payCurrency"],
4091                    "couponType": item["couponType"],
4092                    "couponPeriod": item["couponPeriod"],
4093                    "fixDate": item["fixDate"],
4094                    "couponStartDate": item["couponStartDate"],
4095                    "couponEndDate": item["couponEndDate"],
4096                }
4097
4098                if calendar is None:
4099                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4100
4101                else:
4102                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4103
4104        if calendar is not None:
4105            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4106
4107            # Saving calendar from Pandas DataFrame to XLSX sheet:
4108            if xlsx:
4109                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4110
4111                with pd.ExcelWriter(
4112                        path=xlsxCalendarFile,
4113                        date_format=TKS_DATE_FORMAT,
4114                        datetime_format=TKS_DATE_TIME_FORMAT,
4115                        mode="w",
4116                ) as writer:
4117                    humanReadable = calendar.copy(deep=True)
4118                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4119                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4120                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4121                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4122                    humanReadable.columns = colNames  # human-readable column names
4123
4124                    humanReadable.to_excel(
4125                        writer,
4126                        sheet_name="Bond payments calendar",
4127                        index=False,
4128                        encoding="UTF-8",
4129                        freeze_panes=(1, 2),
4130                    )  # saving as XLSX-file with freeze first row and column as headers
4131
4132                    del humanReadable  # release df in memory
4133
4134                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4135
4136        return calendar
4137
4138    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4139        """
4140        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4141        Also, creates Markdown file with calendar data, `calendar.md` by default.
4142
4143        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4144
4145        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4146                        extended information about bonds: main info, current prices, bond payment calendar,
4147                        coupon yields, current yields and some statistics etc.
4148                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4149        :param show: if `True` then also printing bonds payment calendar to the console,
4150                     otherwise save to file `calendarFile` only. `False` by default.
4151        :return: multilines text in Markdown format with bonds payment calendar as a table.
4152        """
4153        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4154            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4155
4156        infoText = "# Bond payments calendar\n\n"
4157
4158        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4159
4160        if not (calendar is None or calendar.empty):
4161            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4162
4163            info = [
4164                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4165                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4166            ]
4167
4168            newMonth = False
4169            notOneBond = calendar["figi"].nunique() > 1
4170            for i, bond in enumerate(calendar.iterrows()):
4171                if newMonth and notOneBond:
4172                    info.append(splitLine)
4173
4174                info.append(
4175                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4176                        "  √" if bond[1]["paid"] else "  —",
4177                        bond[1]["couponDate"].split("T")[0],
4178                        bond[1]["figi"],
4179                        bond[1]["ticker"],
4180                        bond[1]["couponNumber"],
4181                        "{} {}".format(
4182                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4183                            bond[1]["payCurrency"],
4184                        ),
4185                        bond[1]["couponType"],
4186                        bond[1]["couponPeriod"],
4187                        bond[1]["fixDate"].split("T")[0],
4188                    )
4189                )
4190
4191                if i < len(calendar.values) - 1:
4192                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4193                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4194                    newMonth = False if curDate.month == nextDate.month else True
4195
4196                else:
4197                    newMonth = False
4198
4199            infoText += "".join(info)
4200
4201            if show:
4202                uLogger.info("{}".format(infoText))
4203
4204            if self.calendarFile is not None:
4205                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4206                    fH.write(infoText)
4207
4208                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4209
4210        else:
4211            infoText += "No data\n"
4212
4213        return infoText
4214
4215    def OverviewAccounts(self, show: bool = False) -> dict:
4216        """
4217        Method for parsing and show simple table with all available user accounts.
4218
4219        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4220
4221        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4222        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4223                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4224                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4225                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4226                                                        "closed": "—", "access": "Full access" }, ...}}`
4227        """
4228        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4229
4230        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4231        accounts = {
4232            item["id"]: {
4233                "type": TKS_ACCOUNT_TYPES[item["type"]],
4234                "name": item["name"],
4235                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4236                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4237                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4238                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4239            } for item in rawAccounts["accounts"]
4240        }
4241
4242        # Raw and parsed data with some fields replaced in "stat" section:
4243        view = {
4244            "rawAccounts": rawAccounts,
4245            "stat": accounts,
4246        }
4247
4248        # --- Prepare simple text table with only accounts data in human-readable format:
4249        if show:
4250            info = [
4251                "# User accounts\n\n",
4252                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4253                "| Account ID   | Type                      | Status                    | Name                           |\n",
4254                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4255            ]
4256
4257            for account in view["stat"].keys():
4258                info.extend([
4259                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4260                        account,
4261                        view["stat"][account]["type"],
4262                        view["stat"][account]["status"],
4263                        view["stat"][account]["name"],
4264                    )
4265                ])
4266
4267            infoText = "".join(info)
4268
4269            uLogger.info(infoText)
4270
4271            if self.userAccountsFile:
4272                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4273                    fH.write(infoText)
4274
4275                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4276
4277        return view
4278
4279    def OverviewUserInfo(self, show: bool = False) -> dict:
4280        """
4281        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4282
4283        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4284
4285        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4286        :return: dict with raw parsed data from server and some calculated statistics about it.
4287        """
4288        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4289        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4290        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4291        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4292        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4293        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4294
4295        # This is dict with parsed common user data:
4296        userInfo = {
4297            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4298            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4299            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4300            "tariff": rawUserInfo["tariff"],
4301        }
4302
4303        # This is an array of dict with parsed margin statuses for every account IDs:
4304        margins = {}
4305        for accountId in accounts.keys():
4306            if rawMargins[accountId]:
4307                margins[accountId] = {
4308                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4309                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4310                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4311                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4312                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4313                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4314                }
4315
4316            else:
4317                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4318
4319        unary = {}  # unary-connection limits
4320        for item in rawTariffLimits["unaryLimits"]:
4321            if item["limitPerMinute"] in unary.keys():
4322                unary[item["limitPerMinute"]].extend(item["methods"])
4323
4324            else:
4325                unary[item["limitPerMinute"]] = item["methods"]
4326
4327        stream = {}  # stream-connection limits
4328        for item in rawTariffLimits["streamLimits"]:
4329            if item["limit"] in stream.keys():
4330                stream[item["limit"]].extend(item["streams"])
4331
4332            else:
4333                stream[item["limit"]] = item["streams"]
4334
4335        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4336        limits = {
4337            "unary": unary,
4338            "stream": stream,
4339        }
4340
4341        # Raw and parsed data as an output result:
4342        view = {
4343            "rawUserInfo": rawUserInfo,
4344            "rawAccounts": rawAccounts,
4345            "rawMargins": rawMargins,
4346            "rawTariffLimits": rawTariffLimits,
4347            "stat": {
4348                "userInfo": userInfo,
4349                "accounts": accounts,
4350                "margins": margins,
4351                "limits": limits,
4352            },
4353        }
4354
4355        # --- Prepare text table with user information in human-readable format:
4356        if show:
4357            info = [
4358                "# Full user information\n\n",
4359                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4360                "## Common information\n\n",
4361                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4362                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4363                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4364                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4365                "\n## User accounts\n\n",
4366            ]
4367
4368            for account in view["stat"]["accounts"].keys():
4369                info.extend([
4370                    "### ID: [{}]\n\n".format(account),
4371                    "| Parameters           | Values                                                       |\n",
4372                    "|----------------------|--------------------------------------------------------------|\n",
4373                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4374                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4375                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4376                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4377                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4378                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4379                ])
4380
4381                if margins[account]:
4382                    info.extend([
4383                        "| Margin status:       | Enabled                                                      |\n",
4384                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4385                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4386                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4387                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4388                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4389                    ])
4390
4391                else:
4392                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4393
4394            info.extend([
4395                "\n## Current user tariff limits\n",
4396                "\nSee also:\n",
4397                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4398                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4399                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4400                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4401                "\n### Unary limits\n",
4402            ])
4403
4404            if unary:
4405                for key, values in sorted(unary.items()):
4406                    info.append("\n* Max requests per minute: {}\n".format(key))
4407
4408                    for value in values:
4409                        info.append("  - {}\n".format(value))
4410
4411            else:
4412                info.append("\nNot available\n")
4413
4414            info.append("\n### Stream limits\n")
4415
4416            if stream:
4417                for key, values in sorted(stream.items()):
4418                    info.append("\n* Max stream connections: {}\n".format(key))
4419
4420                    for value in values:
4421                        info.append("  - {}\n".format(value))
4422
4423            else:
4424                info.append("\nNot available\n")
4425
4426            infoText = "".join(info)
4427
4428            uLogger.info(infoText)
4429
4430            if self.userInfoFile:
4431                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4432                    fH.write(infoText)
4433
4434                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4435
4436        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
198    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
199        """
200        Main class init.
201
202        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
203        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
204                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
205        :param useCache: use default cache file with raw data to use instead of `iList`.
206                         True by default. Cache is auto-update if new day has come.
207                         If you don't want to use cache and always updates raw data then set `useCache=False`.
208        :param defaultCache: path to default cache file. `dump.json` by default.
209        """
210        if token is None or not token:
211            try:
212                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
213                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
214
215            except KeyError:
216                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
217                raise Exception("Token required")
218
219        else:
220            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
221            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
222
223        if accountId is None or not accountId:
224            try:
225                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
226                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
227
228            except KeyError:
229                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
230
231        else:
232            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
233            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
234
235        self.version = __version__  # duplicate here used TKSBrokerAPI main version
236        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
237
238        Latest version: https://pypi.org/project/tksbrokerapi/
239        """
240
241        self.aliases = TKS_TICKER_ALIASES
242        """Some aliases instead official tickers.
243
244        See also: `TKSEnums.TKS_TICKER_ALIASES`
245        """
246
247        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
248
249        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
250
251        self.ticker = ""
252        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
253
254        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
255        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
256
257        See also: `SearchByTicker()`, `SearchInstruments()`.
258        """
259
260        self.figi = ""
261        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
262
263        See also: `SearchByFIGI()`, `SearchInstruments()`.
264        """
265
266        self.depth = 1
267        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
268
269        See also: `GetCurrentPrices()`.
270        """
271
272        self.server = r"https://invest-public-api.tinkoff.ru/rest"
273        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
274
275        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
276        """
277
278        uLogger.debug("Broker API server: {}".format(self.server))
279
280        self.timeout = 15
281        """Server operations timeout in seconds. Default: `15`.
282
283        See also: `SendAPIRequest()`.
284        """
285
286        self.headers = {
287            "Content-Type": "application/json",
288            "accept": "application/json",
289            "Authorization": "Bearer {}".format(self.token),
290            "x-app-name": "Tim55667757.TKSBrokerAPI",
291        }
292        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
293
294        See also: `SendAPIRequest()`.
295        """
296
297        self.body = None
298        """Request body which send to broker server. Default: `None`.
299
300        See also: `SendAPIRequest()`.
301        """
302
303        self.moreDebug = False
304        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
305
306        self.historyFile = None
307        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
308
309        See also: `History()`.
310        """
311
312        self.htmlHistoryFile = "index.html"
313        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
314
315        See also: `ShowHistoryChart()`.
316        """
317
318        self.instrumentsFile = "instruments.md"
319        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
320
321        See also: `ShowInstrumentsInfo()`.
322        """
323
324        self.searchResultsFile = "search-results.md"
325        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
326
327        See also: `SearchInstruments()`.
328        """
329
330        self.pricesFile = "prices.md"
331        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
332
333        See also: `GetListOfPrices()`.
334        """
335
336        self.infoFile = "info.md"
337        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
338
339        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
340        """
341
342        self.bondsXLSXFile = "ext-bonds.xlsx"
343        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
344        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
345
346        See also: `ExtendBondsData()`.
347        """
348
349        self.calendarFile = "calendar.md"
350        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
351        
352        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
353
354        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
355        """
356
357        self.overviewFile = "overview.md"
358        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
359
360        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
361        """
362
363        self.overviewDigestFile = "overview-digest.md"
364        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
365
366        See also: `Overview()` with parameter `details="digest"`.
367        """
368
369        self.overviewPositionsFile = "overview-positions.md"
370        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
371
372        See also: `Overview()` with parameter `details="positions"`.
373        """
374
375        self.overviewOrdersFile = "overview-orders.md"
376        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
377
378        See also: `Overview()` with parameter `details="orders"`.
379        """
380
381        self.overviewAnalyticsFile = "overview-analytics.md"
382        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
383
384        See also: `Overview()` with parameter `details="analytics"`.
385        """
386
387        self.overviewBondsCalendarFile = "overview-calendar.md"
388        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
389
390        See also: `Overview()` with parameter `details="calendar"`.
391        """
392
393        self.reportFile = "deals.md"
394        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
395
396        See also: `Deals()`.
397        """
398
399        self.withdrawalLimitsFile = "limits.md"
400        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
401
402        See also: `OverviewLimits()` and `RequestLimits()`.
403        """
404
405        self.userInfoFile = "user-info.md"
406        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
407
408        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
409        """
410
411        self.userAccountsFile = "accounts.md"
412        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
413
414        See also: `OverviewAccounts()`, `RequestAccounts()`.
415        """
416
417        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
418        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
419
420        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
421
422        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
423        """
424
425        self.iList = None  # init iList for raw instruments data
426        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
427        
428        See also: `Listing()`, `DumpInstruments()`.
429        """
430
431        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
432        if useCache:
433            if os.path.exists(self.iListDumpFile):
434                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
435                curTime = datetime.now(tzutc())
436
437                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
438                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
439
440                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
441
442                else:
443                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
444
445                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
446                        os.path.abspath(self.iListDumpFile),
447                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
448                    ))
449
450            else:
451                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
452                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
453
454        else:
455            self.iList = self.Listing()  # request new raw instruments data from broker server
456            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
457
458        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
459        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
460
461        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
462        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
478    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
479        """
480        Send GET or POST request to broker server and receive JSON object.
481
482        self.header: must be defining with dictionary of headers.
483        self.body: if define then used as request body. None by default.
484        self.timeout: global request timeout, 15 seconds by default.
485        :param url: url with REST request.
486        :param reqType: send "GET" or "POST" request. "GET" by default.
487        :param retry: how many times retry after first request if an 5xx server errors occurred.
488        :param pause: sleep time in seconds between retries.
489        :return: response JSON (dictionary) from broker.
490        """
491        if reqType not in ("GET", "POST"):
492            uLogger.error("You can define request type: 'GET' or 'POST'!")
493            raise Exception("Incorrect value")
494
495        if self.moreDebug:
496            uLogger.debug("Request parameters:")
497            uLogger.debug("    - REST API URL: {}".format(url))
498            uLogger.debug("    - request type: {}".format(reqType))
499            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
500            uLogger.debug("    - body:\n{}".format(self.body))
501
502        # fast hack to avoid all operations with some tickers/FIGI
503        responseJSON = {}
504        oK = True
505        for item in self.exclude:
506            if item in url:
507                if self.moreDebug:
508                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
509
510                oK = False
511                break
512
513        if oK:
514            counter = 0
515            response = None
516            errMsg = ""
517
518            while not response and counter <= retry:
519                if reqType == "GET":
520                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
521
522                if reqType == "POST":
523                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
524
525                if self.moreDebug:
526                    uLogger.debug("Response:")
527                    uLogger.debug("    - status code: {}".format(response.status_code))
528                    uLogger.debug("    - reason: {}".format(response.reason))
529                    uLogger.debug("    - body length: {}".format(len(response.text)))
530                    uLogger.debug("    - headers:\n{}".format(response.headers))
531
532                # Server returns some headers:
533                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
534                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
535                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
536                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
537                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
538                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
539                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
540                    sleep(rateLimitWait)
541
542                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
543                if 400 <= response.status_code < 500:
544                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
545                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
546                    counter = retry + 1
547
548                if 500 <= response.status_code < 600:
549                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
550                    uLogger.debug("    - not oK, {}".format(errMsg))
551                    counter += 1
552
553                    if counter <= retry:
554                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
555                        sleep(pause)
556
557            responseJSON = self._ParseJSON(rawData=response.text)
558
559            if errMsg:
560                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
561                uLogger.error("    - not oK, {}".format(errMsg))
562
563        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
596    def Listing(self) -> dict:
597        """
598        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
599
600        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
601        """
602        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
603        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
604
605        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
606        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
607        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
608
609        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
610        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
611        poolUpdater.close()
612
613        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
614        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
615        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
616
617        # calculate minimum price increment (step) for all instruments and set up instrument's type:
618        for iType in iList.keys():
619            for ticker in iList[iType]:
620                iList[iType][ticker]["type"] = iType
621
622                if "minPriceIncrement" in iList[iType][ticker].keys():
623                    iList[iType][ticker]["step"] = NanoToFloat(
624                        iList[iType][ticker]["minPriceIncrement"]["units"],
625                        iList[iType][ticker]["minPriceIncrement"]["nano"],
626                    )
627
628                else:
629                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
630
631        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
633    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
634        """
635        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
636
637        See also: `DumpInstruments()`, `Listing()`.
638
639        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
640                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
641        """
642        if self.iListDumpFile is None or not self.iListDumpFile:
643            uLogger.error("Output name of dump file must be defined!")
644            raise Exception("Filename required")
645
646        if not self.iList or forceUpdate:
647            self.iList = self.Listing()
648
649        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
650
651        # Save as XLSX with separated sheets for every type of instruments:
652        with pd.ExcelWriter(
653                path=xlsxDumpFile,
654                date_format=TKS_DATE_FORMAT,
655                datetime_format=TKS_DATE_TIME_FORMAT,
656                mode="w",
657        ) as writer:
658            for iType in TKS_INSTRUMENTS:
659                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
660                df = df[sorted(df)]  # sorted by column names
661                df = df.applymap(
662                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
663                    na_action="ignore",
664                )  # converting numbers from nano-type to float in every cell
665                df.to_excel(
666                    writer,
667                    sheet_name=iType,
668                    encoding="UTF-8",
669                    freeze_panes=(1, 1),
670                )  # saving as XLSX-file with freeze first row and column as headers
671
672        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
674    def DumpInstruments(self, forceUpdate: bool = True) -> str:
675        """
676        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
677        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
678
679        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
680
681        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
682                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
683        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
684        """
685        if self.iListDumpFile is None or not self.iListDumpFile:
686            uLogger.error("Output name of dump file must be defined!")
687            raise Exception("Filename required")
688
689        if not self.iList or forceUpdate:
690            self.iList = self.Listing()
691
692        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
693        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
694            fH.write(jsonDump)
695
696        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
697
698        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
700    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
701        """
702        Show information about one instrument defined by json data and prints it in Markdown format.
703
704        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
705
706        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
707        :param show: if `True` then also printing information about instrument and its current price.
708        :return: multilines text in Markdown format with information about one instrument.
709        """
710        splitLine = "|                                                             |                                                        |\n"
711        infoText = ""
712
713        if iJSON is not None and iJSON and isinstance(iJSON, dict):
714            info = [
715                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
716                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
717                "| Parameters                                                  | Values                                                 |\n",
718                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
719                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
720                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
721            ]
722
723            if "sector" in iJSON.keys() and iJSON["sector"]:
724                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
725
726            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
727                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
728                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
729            )))
730
731            info.extend([
732                splitLine,
733                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
734                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
735            ])
736
737            if "isin" in iJSON.keys() and iJSON["isin"]:
738                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
739
740            if "classCode" in iJSON.keys():
741                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
742
743            info.extend([
744                splitLine,
745                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
746                splitLine,
747                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
748                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
749                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
750            ])
751
752            if iJSON["figi"]:
753                self.figi = iJSON["figi"]
754                iJSON = iJSON | self.RequestTradingStatus()
755
756                info.extend([
757                    splitLine,
758                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
759                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
760                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
761                ])
762
763            info.append(splitLine)
764
765            if "type" in iJSON.keys() and iJSON["type"]:
766                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
767
768            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
769                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
770
771            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
772                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
773
774            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
775                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
776
777            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
778                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
779
780            if "focusType" in iJSON.keys() and iJSON["focusType"]:
781                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
782
783            if "assetType" in iJSON.keys() and iJSON["assetType"]:
784                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
785
786            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
787                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
788
789            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
790                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
791
792            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
793                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
794
795            if "currency" in iJSON.keys():
796                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
797
798            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
799                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
800
801            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
802                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
803
804            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
805                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
806
807            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
808                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
809
810            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
811                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
812
813            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
814                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
815
816            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
817                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
818
819            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
820                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
821
822            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
823                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
824
825            iExt = None
826            if iJSON["type"] == "Bonds":
827                info.extend([
828                    splitLine,
829                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
830                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
831                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
832                        iJSON["nominal"]["currency"],
833                    )),
834                ])
835
836                if "floatingCouponFlag" in iJSON.keys():
837                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
838
839                if "amortizationFlag" in iJSON.keys():
840                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
841
842                info.append(splitLine)
843
844                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
845                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
846
847                if iJSON["figi"]:
848                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
849
850                    info.extend([
851                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
852                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
853                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
854                    ])
855
856                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
857                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
858                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
859                        iJSON["aciValue"]["currency"]
860                    )))
861
862            if "currentPrice" in iJSON.keys():
863                info.append(splitLine)
864
865                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
866                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
867
868                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
869                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
870                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
871                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
872                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
873
874                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
875                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
876
877                info.extend([
878                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
879                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
880                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
881                    )),
882                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
883                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
884                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
885                    )),
886                    "| Changes between last deal price and last close              | {:<54} |\n".format(
887                        "{:.2f}%{}".format(
888                            iJSON["currentPrice"]["changes"],
889                            " ({}{:.2f} {})".format(
890                                "+" if bondChangesDelta > 0 else "",
891                                bondChangesDelta,
892                                aciCurrency
893                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
894                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
895                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
896                                currency
897                            ),
898                        )
899                    ),
900                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
901                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
902                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
903                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
904                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
905                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
906                    )),
907                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
908                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
909                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
910                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
911                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
912                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
913                    )),
914                ])
915
916            if "lot" in iJSON.keys():
917                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
918
919            if "step" in iJSON.keys() and iJSON["step"] != 0:
920                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
921
922            # Add bond payment calendar:
923            if iJSON["type"] == "Bonds":
924                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
925                info.extend(["\n", strCalendar])
926
927            infoText += "".join(info)
928
929            if show:
930                uLogger.info("{}".format(infoText))
931
932            else:
933                uLogger.debug("{}".format(infoText))
934
935            if self.infoFile is not None:
936                with open(self.infoFile, "w", encoding="UTF-8") as fH:
937                    fH.write(infoText)
938
939                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
940
941        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 943    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 944        """
 945        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 946
 947        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 948        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 949        :return: JSON formatted data with information about instrument.
 950        """
 951        tickerJSON = {}
 952        if self.moreDebug:
 953            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 954
 955        if not self.ticker:
 956            uLogger.warning("self.ticker variable is not be empty!")
 957
 958        else:
 959            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 960                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 961                raise Exception("Instrument not allowed")
 962
 963            if not self.iList:
 964                self.iList = self.Listing()
 965
 966            if self.ticker in self.iList["Shares"].keys():
 967                tickerJSON = self.iList["Shares"][self.ticker]
 968                if self.moreDebug:
 969                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 970
 971            elif self.ticker in self.iList["Currencies"].keys():
 972                tickerJSON = self.iList["Currencies"][self.ticker]
 973                if self.moreDebug:
 974                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 975
 976            elif self.ticker in self.iList["Bonds"].keys():
 977                tickerJSON = self.iList["Bonds"][self.ticker]
 978                if self.moreDebug:
 979                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 980
 981            elif self.ticker in self.iList["Etfs"].keys():
 982                tickerJSON = self.iList["Etfs"][self.ticker]
 983                if self.moreDebug:
 984                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 985
 986            elif self.ticker in self.iList["Futures"].keys():
 987                tickerJSON = self.iList["Futures"][self.ticker]
 988                if self.moreDebug:
 989                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 990
 991        if tickerJSON:
 992            self.figi = tickerJSON["figi"]
 993
 994            if requestPrice:
 995                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 996
 997                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 998                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 999
1000                else:
1001                    tickerJSON["currentPrice"]["changes"] = 0
1002
1003            if show:
1004                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
1005
1006        else:
1007            if show:
1008                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
1009
1010        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1012    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
1013        """
1014        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
1015
1016        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1017        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1018        :return: JSON formatted data with information about instrument.
1019        """
1020        figiJSON = {}
1021        if self.moreDebug:
1022            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1023
1024        if not self.figi:
1025            uLogger.warning("self.figi variable is not be empty!")
1026
1027        else:
1028            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1029                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1030                raise Exception("Instrument not allowed")
1031
1032            if not self.iList:
1033                self.iList = self.Listing()
1034
1035            for item in self.iList["Shares"].keys():
1036                if self.figi == self.iList["Shares"][item]["figi"]:
1037                    figiJSON = self.iList["Shares"][item]
1038
1039                    if self.moreDebug:
1040                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1041
1042                    break
1043
1044            if not figiJSON:
1045                for item in self.iList["Currencies"].keys():
1046                    if self.figi == self.iList["Currencies"][item]["figi"]:
1047                        figiJSON = self.iList["Currencies"][item]
1048
1049                        if self.moreDebug:
1050                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Bonds"].keys():
1056                    if self.figi == self.iList["Bonds"][item]["figi"]:
1057                        figiJSON = self.iList["Bonds"][item]
1058
1059                        if self.moreDebug:
1060                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Etfs"].keys():
1066                    if self.figi == self.iList["Etfs"][item]["figi"]:
1067                        figiJSON = self.iList["Etfs"][item]
1068
1069                        if self.moreDebug:
1070                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1071
1072                        break
1073
1074            if not figiJSON:
1075                for item in self.iList["Futures"].keys():
1076                    if self.figi == self.iList["Futures"][item]["figi"]:
1077                        figiJSON = self.iList["Futures"][item]
1078
1079                        if self.moreDebug:
1080                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1081
1082                        break
1083
1084        if figiJSON:
1085            self.figi = figiJSON["figi"]
1086            self.ticker = figiJSON["ticker"]
1087
1088            if requestPrice:
1089                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1090
1091                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1092                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1093
1094                else:
1095                    figiJSON["currentPrice"]["changes"] = 0
1096
1097            if show:
1098                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1099
1100        else:
1101            if show:
1102                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1103
1104        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1106    def GetCurrentPrices(self, show: bool = True) -> dict:
1107        """
1108        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1109        `{"buy": [{"price": 1243.8, "quantity": 193},
1110                  {"price": 1244.0, "quantity": 168},
1111                  {"price": 1244.8, "quantity": 5},
1112                  {"price": 1245.0, "quantity": 61},
1113                  {"price": 1245.4, "quantity": 60}],
1114          "sell": [{"price": 1243.6, "quantity": 8},
1115                   {"price": 1242.6, "quantity": 10},
1116                   {"price": 1242.4, "quantity": 18},
1117                   {"price": 1242.2, "quantity": 50},
1118                   {"price": 1242.0, "quantity": 113}],
1119          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1120        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1121        - sell: list of dicts with Buyers prices,
1122            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1123            - quantity: volume value by current price in lots,
1124        - limitUp: current trade session limit price, maximum,
1125        - limitDown: current trade session limit price, minimum,
1126        - lastPrice: last deal price of the instrument,
1127        - closePrice: previous trade session close price of the instrument.
1128
1129        See also: `SearchByTicker()` and `SearchByFIGI()`.
1130        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1131        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1132
1133        :param show: if `True` then print DOM to log and console.
1134        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1135                 If an error occurred then returns an empty record:
1136                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1137        """
1138        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1139
1140        if self.depth < 1:
1141            uLogger.error("Depth of Market (DOM) must be >=1!")
1142            raise Exception("Incorrect value")
1143
1144        if not (self.ticker or self.figi):
1145            uLogger.error("self.ticker or self.figi variables must be defined!")
1146            raise Exception("Ticker or FIGI required")
1147
1148        if self.ticker and not self.figi:
1149            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1150            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1151
1152        if not self.ticker and self.figi:
1153            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1154            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1155
1156        if not self.figi:
1157            uLogger.error("FIGI is not defined!")
1158            raise Exception("Ticker or FIGI required")
1159
1160        else:
1161            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1162
1163            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1164            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1165            self.body = str({"figi": self.figi, "depth": self.depth})
1166            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1167
1168            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1169                # list of dicts with sellers orders:
1170                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1171
1172                # list of dicts with buyers orders:
1173                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1174
1175                # max price of instrument at this time:
1176                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1177
1178                # min price of instrument at this time:
1179                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1180
1181                # last price of deal with instrument:
1182                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1183
1184                # last close price of instrument:
1185                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1186
1187            else:
1188                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1189                uLogger.debug("Server response: {}".format(pricesResponse))
1190
1191            if show:
1192                if prices["buy"] or prices["sell"]:
1193                    info = [
1194                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1195                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1196                            self.ticker,
1197                            self.figi,
1198                            self.depth,
1199                        ),
1200                        "-" * 60, "\n",
1201                        "             Orders of Buyers | Orders of Sellers\n",
1202                        "-" * 60, "\n",
1203                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1204                        "-" * 60, "\n",
1205                    ]
1206
1207                    if not prices["buy"]:
1208                        info.append("                              | No orders!\n")
1209                        sumBuy = 0
1210
1211                    else:
1212                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1213                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1214                        for item in maxMinSorted:
1215                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1216
1217                    if not prices["sell"]:
1218                        info.append("No orders!                    |\n")
1219                        sumSell = 0
1220
1221                    else:
1222                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1223                        for item in prices["sell"]:
1224                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1225
1226                    info.extend([
1227                        "-" * 60, "\n",
1228                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1229                        "-" * 60, "\n",
1230                    ])
1231
1232                    infoText = "".join(info)
1233
1234                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1235
1236                else:
1237                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1238
1239        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1241    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1242        """
1243        This method get and show information about all available broker instruments for current user account.
1244        If `instrumentsFile` string is not empty then also save information to this file.
1245
1246        :param show: if `True` then print results to console, if `False` — print only to file.
1247        :return: multi-lines string with all available broker instruments
1248        """
1249        if not self.iList:
1250            self.iList = self.Listing()
1251
1252        info = [
1253            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1254            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1255        ]
1256
1257        # add instruments count by type:
1258        for iType in self.iList.keys():
1259            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1260
1261        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1262        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1263
1264        # generating info tables with all instruments by type:
1265        for iType in self.iList.keys():
1266            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1267
1268            for instrument in self.iList[iType].keys():
1269                iName = self.iList[iType][instrument]["name"]  # instrument's name
1270                if len(iName) > 57:
1271                    iName = "{}...".format(iName[:54])  # right trim for a long string
1272
1273                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1274                    self.iList[iType][instrument]["ticker"],
1275                    iName,
1276                    self.iList[iType][instrument]["figi"],
1277                    self.iList[iType][instrument]["currency"],
1278                    self.iList[iType][instrument]["lot"],
1279                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1280                ))
1281
1282        infoText = "".join(info)
1283
1284        if show:
1285            uLogger.info(infoText)
1286
1287        if self.instrumentsFile:
1288            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1289                fH.write(infoText)
1290
1291            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1292
1293        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1295    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1296        """
1297        This method search and show information about instruments by part of its ticker, FIGI or name.
1298        If `searchResultsFile` string is not empty then also save information to this file.
1299
1300        :param pattern: string with part of ticker, FIGI or instrument's name.
1301        :param show: if `True` then print results to console, if `False` — return list of result only.
1302        :return: list of dictionaries with all found instruments.
1303        """
1304        if not self.iList:
1305            self.iList = self.Listing()
1306
1307        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1308        compiledPattern = re.compile(pattern, re.IGNORECASE)
1309
1310        for iType in self.iList:
1311            for instrument in self.iList[iType].values():
1312                searchResult = compiledPattern.search(" ".join(
1313                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1314                ))
1315
1316                if searchResult:
1317                    searchResults[iType][instrument["ticker"]] = instrument
1318
1319        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1320        info = [
1321            "# Search results\n\n",
1322            "* **Search pattern:** [{}]\n".format(pattern),
1323            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1324            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1325        ]
1326        infoShort = info[:]
1327
1328        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1329        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1330        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1331
1332        if resultsLen == 0:
1333            info.append("\nNo results\n")
1334            infoShort.append("\nNo results\n")
1335            uLogger.warning("No results. Try changing your search pattern.")
1336
1337        else:
1338            for iType in searchResults:
1339                iTypeValuesCount = len(searchResults[iType].values())
1340                if iTypeValuesCount > 0:
1341                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1342                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1343
1344                    for instrument in searchResults[iType].values():
1345                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1346                            instrument["type"],
1347                            instrument["ticker"],
1348                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1349                            instrument["figi"],
1350                        ))
1351
1352                    if iTypeValuesCount <= 5:
1353                        infoShort.extend(info[-iTypeValuesCount:])
1354
1355                    else:
1356                        infoShort.extend(info[-5:])
1357                        infoShort.append(skippedLine)
1358
1359        infoText = "".join(info)
1360        infoTextShort = "".join(infoShort)
1361
1362        if show:
1363            uLogger.info(infoTextShort)
1364            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1365
1366        if self.searchResultsFile:
1367            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1368                fH.write(infoText)
1369
1370            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1371
1372        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1374    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1375        """
1376        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1377
1378        :param instruments: list of strings with tickers or FIGIs.
1379        :return: list with unique instrument FIGIs only.
1380        """
1381        requestedInstruments = []
1382        for iName in instruments:
1383            if iName not in self.aliases.keys():
1384                if iName not in requestedInstruments:
1385                    requestedInstruments.append(iName)
1386
1387            else:
1388                if iName not in requestedInstruments:
1389                    if self.aliases[iName] not in requestedInstruments:
1390                        requestedInstruments.append(self.aliases[iName])
1391
1392        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1393
1394        onlyUniqueFIGIs = []
1395        for iName in requestedInstruments:
1396            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1397                continue
1398
1399            self.ticker = iName
1400            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1401
1402            if not iData:
1403                self.ticker = ""
1404                self.figi = iName
1405
1406                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1407
1408                if not iData:
1409                    self.figi = ""
1410                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1411
1412            if iData and iData["figi"] not in onlyUniqueFIGIs:
1413                onlyUniqueFIGIs.append(iData["figi"])
1414
1415        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1416
1417        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1419    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1420        """
1421        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1422
1423        See limits: https://tinkoff.github.io/investAPI/limits/
1424
1425        If `pricesFile` string is not empty then also save information to this file.
1426
1427        :param instruments: list of strings with tickers or FIGIs.
1428        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1429        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1430                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1431        """
1432        if instruments is None or not instruments:
1433            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1434            raise Exception("Ticker or FIGI required")
1435
1436        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1437
1438        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1439
1440        iList = []  # trying to get info and current prices about all unique instruments:
1441        for self.figi in onlyUniqueFIGIs:
1442            iData = self.SearchByFIGI(requestPrice=True)
1443            iList.append(iData)
1444
1445        self.ShowListOfPrices(iList, show)
1446
1447        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1449    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1450        """
1451        Show table contains current prices of given instruments.
1452
1453        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1454                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1455        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1456        :return: multilines text in Markdown format as a table contains current prices.
1457        """
1458        infoText = ""
1459
1460        if show or self.pricesFile:
1461            info = [
1462                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1463                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1464                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1465            ]
1466
1467            for item in iList:
1468                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1469                    item["ticker"],
1470                    item["figi"],
1471                    item["type"],
1472                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1473                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1474                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1475                    "{} / {}".format(
1476                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1477                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1478                    ),
1479                    "{} / {}".format(
1480                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1481                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1482                    ),
1483                    item["currency"],
1484                ))
1485
1486            infoText = "".join(info)
1487
1488            if show:
1489                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1490
1491            if self.pricesFile:
1492                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1493                    fH.write(infoText)
1494
1495                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1496
1497        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1499    def RequestTradingStatus(self) -> dict:
1500        """
1501        Requesting trading status for the instrument defined by `figi` variable.
1502
1503        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1504
1505        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1506
1507        :return: dictionary with trading status attributes. Response example:
1508                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1509                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1510        """
1511        if self.figi is None or not self.figi:
1512            uLogger.error("Variable `figi` must be defined for using this method!")
1513            raise Exception("FIGI required")
1514
1515        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1516
1517        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1518        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1519        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1520
1521        if self.moreDebug:
1522            uLogger.debug("Records about current trading status successfully received")
1523
1524        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1526    def RequestPortfolio(self) -> dict:
1527        """
1528        Requesting actual user's portfolio for current `accountId`.
1529
1530        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1531
1532        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1533
1534        :return: dictionary with user's portfolio.
1535        """
1536        if self.accountId is None or not self.accountId:
1537            uLogger.error("Variable `accountId` must be defined for using this method!")
1538            raise Exception("Account ID required")
1539
1540        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1541
1542        self.body = str({"accountId": self.accountId})
1543        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1544        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1545
1546        if self.moreDebug:
1547            uLogger.debug("Records about user's portfolio successfully received")
1548
1549        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1551    def RequestPositions(self) -> dict:
1552        """
1553        Requesting open positions by currencies and instruments for current `accountId`.
1554
1555        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1556
1557        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1558
1559        :return: dictionary with open positions by instruments.
1560        """
1561        if self.accountId is None or not self.accountId:
1562            uLogger.error("Variable `accountId` must be defined for using this method!")
1563            raise Exception("Account ID required")
1564
1565        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1566
1567        self.body = str({"accountId": self.accountId})
1568        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1569        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1570
1571        if self.moreDebug:
1572            uLogger.debug("Records about current open positions successfully received")
1573
1574        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1576    def RequestPendingOrders(self) -> list:
1577        """
1578        Requesting current actual pending orders for current `accountId`.
1579
1580        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1581
1582        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1583
1584        :return: list of dictionaries with pending orders.
1585        """
1586        if self.accountId is None or not self.accountId:
1587            uLogger.error("Variable `accountId` must be defined for using this method!")
1588            raise Exception("Account ID required")
1589
1590        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1591
1592        self.body = str({"accountId": self.accountId})
1593        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1594        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1595
1596        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1597
1598        return rawOrders

Requesting current actual pending orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1600    def RequestStopOrders(self) -> list:
1601        """
1602        Requesting current actual stop orders for current `accountId`.
1603
1604        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1605
1606        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1607
1608        :return: list of dictionaries with stop orders.
1609        """
1610        if self.accountId is None or not self.accountId:
1611            uLogger.error("Variable `accountId` must be defined for using this method!")
1612            raise Exception("Account ID required")
1613
1614        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1615
1616        self.body = str({"accountId": self.accountId})
1617        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1618        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1619
1620        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1621
1622        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1624    def Overview(self, show: bool = False, details: str = "full") -> dict:
1625        """
1626        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1627        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1628        and `overviewBondsCalendarFile` are defined then also save information to file.
1629
1630        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1631        many requests about the state of the portfolio, and then, based on the received data, a large number
1632        of calculation and statistics are collected.
1633
1634        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1635        :param details: how detailed should the information be?
1636        - `full` — shows full available information about portfolio status (by default),
1637        - `positions` — shows only open positions,
1638        - `orders` — shows only sections of open limits and stop orders.
1639        - `digest` — show a short digest of the portfolio status,
1640        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1641        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1642        :return: dictionary with client's raw portfolio and some statistics.
1643        """
1644        if self.accountId is None or not self.accountId:
1645            uLogger.error("Variable `accountId` must be defined for using this method!")
1646            raise Exception("Account ID required")
1647
1648        view = {
1649            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1650                "headers": {},  # list of dictionaries, response headers without "positions" section
1651                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1652                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1653                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1654                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1655                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1656                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1657                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1658                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1659                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1660            },
1661            "stat": {  # --- some statistics calculated using "raw" sections:
1662                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1663                "availableRUB": 0.,  # available rubles (without other currencies)
1664                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1665                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1666                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1667                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1668                "sharesCostRUB": 0.,  # costs of all shares in RUB
1669                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1670                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1671                "futuresCostRUB": 0.,  # costs of all futures in RUB
1672                "Currencies": [],  # list of dictionaries of all currencies statistics
1673                "Shares": [],  # list of dictionaries of all shares statistics
1674                "Bonds": [],  # list of dictionaries of all bonds statistics
1675                "Etfs": [],  # list of dictionaries of all etfs statistics
1676                "Futures": [],  # list of dictionaries of all futures statistics
1677                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1678                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1679                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1680                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1681                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1682            },
1683            "analytics": {  # --- some analytics of portfolio:
1684                "distrByAssets": {},  # portfolio distribution by assets
1685                "distrByCompanies": {},  # portfolio distribution by companies
1686                "distrBySectors": {},  # portfolio distribution by sectors
1687                "distrByCurrencies": {},  # portfolio distribution by currencies
1688                "distrByCountries": {},  # portfolio distribution by countries
1689                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1690            }
1691        }
1692
1693        details = details.lower()
1694        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1695        if details not in availableDetails:
1696            details = "full"
1697            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1698
1699        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1700
1701        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1702        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1703        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1704        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1705
1706        # save response headers without "positions" section:
1707        for key in portfolioResponse.keys():
1708            if key != "positions":
1709                view["raw"]["headers"][key] = portfolioResponse[key]
1710
1711            else:
1712                continue
1713
1714        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1715        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1716        for item in portfolioResponse["positions"]:
1717            if item["instrumentType"] == "currency":
1718                self.figi = item["figi"]
1719                curr = self.SearchByFIGI(requestPrice=False)
1720
1721                # current price of currency in RUB:
1722                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1723                    "name": curr["name"],
1724                    "currentPrice": NanoToFloat(
1725                        item["currentPrice"]["units"],
1726                        item["currentPrice"]["nano"]
1727                    ),
1728                }
1729
1730                view["raw"]["Currencies"].append(item)
1731
1732            elif item["instrumentType"] == "share":
1733                view["raw"]["Shares"].append(item)
1734
1735            elif item["instrumentType"] == "bond":
1736                view["raw"]["Bonds"].append(item)
1737
1738            elif item["instrumentType"] == "etf":
1739                view["raw"]["Etfs"].append(item)
1740
1741            elif item["instrumentType"] == "futures":
1742                view["raw"]["Futures"].append(item)
1743
1744            else:
1745                continue
1746
1747        # how many volume of currencies (by ISO currency name) are blocked:
1748        for item in view["raw"]["positions"]["blocked"]:
1749            blocked = NanoToFloat(item["units"], item["nano"])
1750            if blocked > 0:
1751                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1752
1753        # how many volume of instruments (by FIGI) are blocked:
1754        for item in view["raw"]["positions"]["securities"]:
1755            blocked = int(item["blocked"])
1756            if blocked > 0:
1757                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1758
1759        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1760
1761        if "rub" in allBlocked.keys():
1762            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1763
1764        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1765        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1766        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1767        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1768        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1769        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1770        view["stat"]["portfolioCostRUB"] = sum([
1771            view["stat"]["allCurrenciesCostRUB"],
1772            view["stat"]["sharesCostRUB"],
1773            view["stat"]["bondsCostRUB"],
1774            view["stat"]["etfsCostRUB"],
1775            view["stat"]["futuresCostRUB"],
1776        ])
1777
1778        # --- calculating some portfolio statistics:
1779        byComp = {}  # distribution by companies
1780        bySect = {}  # distribution by sectors
1781        byCurr = {}  # distribution by currencies (include RUB)
1782        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1783        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1784
1785        for item in portfolioResponse["positions"]:
1786            self.figi = item["figi"]
1787            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1788
1789            if instrument:
1790                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1791                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1792
1793                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1794                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1795
1796                else:
1797                    blocked = 0
1798
1799                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1800                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1801                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1802                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1803                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1804                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1805                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1806                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1807                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1808                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1809                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1810                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1811
1812                statData = {
1813                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1814                    "ticker": instrument["ticker"],  # ticker by FIGI
1815                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1816                    "volume": volume,  # available volume of instrument
1817                    "lots": lots,  # volume in lots of instrument
1818                    "direction": direction,  # direction of an instrument's position: short or long
1819                    "blocked": blocked,  # blocked volume of currency or instrument
1820                    "currentPrice": curPrice,  # current instrument's price in basic asset
1821                    "average": average,  # current average position price
1822                    "cost": cost,  # current cost of all volume of instrument in basic asset
1823                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1824                    "costRUB": costRUB,  # cost of instrument in ruble
1825                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1826                    "profit": profit,  # expected profit at current moment
1827                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1828                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1829                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1830                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1831                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1832                    "step": instrument["step"],  # minimum price increment
1833                }
1834
1835                # adding distribution by unique countries:
1836                if statData["country"] not in byCountry.keys():
1837                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1838
1839                else:
1840                    byCountry[statData["country"]]["cost"] += costRUB
1841                    byCountry[statData["country"]]["percent"] += percentCostRUB
1842
1843                if item["instrumentType"] != "currency":
1844                    # adding distribution by unique companies:
1845                    if statData["name"]:
1846                        if statData["name"] not in byComp.keys():
1847                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1848
1849                        else:
1850                            byComp[statData["name"]]["cost"] += costRUB
1851                            byComp[statData["name"]]["percent"] += percentCostRUB
1852
1853                    # adding distribution by unique sectors:
1854                    if statData["sector"] not in bySect.keys():
1855                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1856
1857                    else:
1858                        bySect[statData["sector"]]["cost"] += costRUB
1859                        bySect[statData["sector"]]["percent"] += percentCostRUB
1860
1861                # adding distribution by unique currencies:
1862                if currency not in byCurr.keys():
1863                    byCurr[currency] = {
1864                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1865                        "cost": costRUB,
1866                        "percent": percentCostRUB
1867                    }
1868
1869                else:
1870                    byCurr[currency]["cost"] += costRUB
1871                    byCurr[currency]["percent"] += percentCostRUB
1872
1873                # saving statistics for every instrument:
1874                if item["instrumentType"] == "currency":
1875                    view["stat"]["Currencies"].append(statData)
1876
1877                    # update dict with free funds for trading (total - blocked) by currencies
1878                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1879                    view["stat"]["funds"][currency] = {
1880                        "total": volume,
1881                        "totalCostRUB": costRUB,  # total volume cost in rubles
1882                        "free": volume - blocked,
1883                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1884                    }
1885
1886                elif item["instrumentType"] == "share":
1887                    view["stat"]["Shares"].append(statData)
1888
1889                elif item["instrumentType"] == "bond":
1890                    view["stat"]["Bonds"].append(statData)
1891
1892                elif item["instrumentType"] == "etf":
1893                    view["stat"]["Etfs"].append(statData)
1894
1895                elif item["instrumentType"] == "Futures":
1896                    view["stat"]["Futures"].append(statData)
1897
1898                else:
1899                    continue
1900
1901        # total changes in Russian Ruble:
1902        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1903        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1904        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1905        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1906        view["stat"]["funds"]["rub"] = {
1907            "total": view["stat"]["availableRUB"],
1908            "totalCostRUB": view["stat"]["availableRUB"],
1909            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1910            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1911        }
1912
1913        # --- pending orders sector data:
1914        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1915        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1916
1917        for item in view["raw"]["orders"]:
1918            self.figi = item["figi"]
1919
1920            if item["figi"] not in uniquePendingOrdersFIGIs:
1921                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1922
1923                uniquePendingOrdersFIGIs.append(item["figi"])
1924                uniquePendingOrders[item["figi"]] = instrument
1925
1926            else:
1927                instrument = uniquePendingOrders[item["figi"]]
1928
1929            if instrument:
1930                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1931                orderType = TKS_ORDER_TYPES[item["orderType"]]
1932                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1933                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1934
1935                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1936                if item["direction"] == "ORDER_DIRECTION_BUY":
1937                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1938
1939                else:
1940                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1941
1942                # requested price for order execution:
1943                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1944
1945                # necessary changes in percent to reach target from current price:
1946                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1947
1948                view["stat"]["orders"].append({
1949                    "orderID": item["orderId"],  # orderId number parameter of current order
1950                    "figi": item["figi"],  # FIGI identification
1951                    "ticker": instrument["ticker"],  # ticker name by FIGI
1952                    "lotsRequested": item["lotsRequested"],  # requested lots value
1953                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1954                    "currentPrice": lastPrice,  # current instrument's price for defined action
1955                    "targetPrice": target,  # requested price for order execution in base currency
1956                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1957                    "percentChanges": changes,  # changes in percent to target from current price
1958                    "currency": item["currency"],  # instrument's currency name
1959                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1960                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1961                    "status": orderState,  # order status from TKS_ORDER_STATES
1962                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1963                })
1964
1965        # --- stop orders sector data:
1966        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1967        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1968
1969        for item in view["raw"]["stopOrders"]:
1970            self.figi = item["figi"]
1971
1972            if item["figi"] not in uniqueStopOrdersFIGIs:
1973                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1974
1975                uniqueStopOrdersFIGIs.append(item["figi"])
1976                uniqueStopOrders[item["figi"]] = instrument
1977
1978            else:
1979                instrument = uniqueStopOrders[item["figi"]]
1980
1981            if instrument:
1982                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1983                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1984                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1985
1986                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1987                if "expirationTime" in item.keys():
1988                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1989                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1990
1991                else:
1992                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1993                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1994
1995                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1996                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1997                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1998
1999                else:
2000                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2001
2002                # requested price when stop-order executed:
2003                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2004
2005                # price for limit-order, set up when stop-order executed:
2006                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2007
2008                # necessary changes in percent to reach target from current price:
2009                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2010
2011                view["stat"]["stopOrders"].append({
2012                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2013                    "figi": item["figi"],  # FIGI identification
2014                    "ticker": instrument["ticker"],  # ticker name by FIGI
2015                    "lotsRequested": item["lotsRequested"],  # requested lots value
2016                    "currentPrice": lastPrice,  # current instrument's price for defined action
2017                    "targetPrice": target,  # requested price for stop-order execution in base currency
2018                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2019                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2020                    "percentChanges": changes,  # changes in percent to target from current price
2021                    "currency": item["currency"],  # instrument's currency name
2022                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2023                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2024                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2025                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2026                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2027                })
2028
2029        # --- calculating data for analytics section:
2030        # portfolio distribution by assets:
2031        view["analytics"]["distrByAssets"] = {
2032            "Ruble": {
2033                "uniques": 1,
2034                "cost": view["stat"]["availableRUB"],
2035                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2036            },
2037            "Currencies": {
2038                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2039                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2040                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041            },
2042            "Shares": {
2043                "uniques": len(view["stat"]["Shares"]),
2044                "cost": view["stat"]["sharesCostRUB"],
2045                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046            },
2047            "Bonds": {
2048                "uniques": len(view["stat"]["Bonds"]),
2049                "cost": view["stat"]["bondsCostRUB"],
2050                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2051            },
2052            "Etfs": {
2053                "uniques": len(view["stat"]["Etfs"]),
2054                "cost": view["stat"]["etfsCostRUB"],
2055                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2056            },
2057            "Futures": {
2058                "uniques": len(view["stat"]["Futures"]),
2059                "cost": view["stat"]["futuresCostRUB"],
2060                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2061            },
2062        }
2063
2064        # portfolio distribution by companies:
2065        view["analytics"]["distrByCompanies"]["All money cash"] = {
2066            "ticker": "",
2067            "cost": view["stat"]["allCurrenciesCostRUB"],
2068            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2069        }
2070        view["analytics"]["distrByCompanies"].update(byComp)
2071
2072        # portfolio distribution by sectors:
2073        view["analytics"]["distrBySectors"]["All money cash"] = {
2074            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2075            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2076        }
2077        view["analytics"]["distrBySectors"].update(bySect)
2078
2079        # portfolio distribution by currencies:
2080        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2081            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2082
2083            if self.moreDebug:
2084                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2085
2086        view["analytics"]["distrByCurrencies"].update(byCurr)
2087        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2088        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2089
2090        # portfolio distribution by countries:
2091        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2092            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2093
2094            if self.moreDebug:
2095                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2096
2097        view["analytics"]["distrByCountries"].update(byCountry)
2098        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2099        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2100
2101        # --- Prepare text statistics overview in human-readable:
2102        if show:
2103            # Whatever the value `details`, header not changes:
2104            info = [
2105                "# Client's portfolio\n\n",
2106                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2107                "* **Account ID:** [{}]\n".format(self.accountId),
2108            ]
2109
2110            if details in ["full", "positions", "digest"]:
2111                info.extend([
2112                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2113                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2114                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2115                        view["stat"]["totalChangesRUB"],
2116                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2117                        view["stat"]["totalChangesPercentRUB"],
2118                    ),
2119                ])
2120
2121            if details in ["full", "positions"]:
2122                info.extend([
2123                    "## Open positions\n\n",
2124                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2125                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2126                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2127                        "{:.2f} ({:.2f}) rub".format(
2128                            view["stat"]["availableRUB"],
2129                            view["stat"]["blockedRUB"],
2130                        )
2131                    )
2132                ])
2133
2134                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2135                    return [
2136                        "|                             |                                 |          |              |              |                     |                              |\n",
2137                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2138                            noTradeStr if noTradeStr else typeStr,
2139                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2140                        ),
2141                    ]
2142
2143                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2144                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2145                        "{} [{}]".format(data["ticker"], data["figi"]),
2146                        "{:.2f} ({:.2f}) {}".format(
2147                            data["volume"],
2148                            data["blocked"],
2149                            data["currency"],
2150                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2151                            data["volume"],
2152                            data["blocked"],
2153                        ),
2154                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2155                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2156                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2157                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2158                        "{}{:.2f} {} ({}{:.2f}%)".format(
2159                            "+" if data["profit"] > 0 else "",
2160                            data["profit"], data["baseCurrencyName"],
2161                            "+" if data["percentProfit"] > 0 else "",
2162                            data["percentProfit"],
2163                        ),
2164                    )
2165
2166                # --- Show currencies section:
2167                if view["stat"]["Currencies"]:
2168                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2169                    for item in view["stat"]["Currencies"]:
2170                        info.append(_InfoStr(item, showCurrencyName=True))
2171
2172                else:
2173                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2174
2175                # --- Show shares section:
2176                if view["stat"]["Shares"]:
2177                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2178
2179                    for item in view["stat"]["Shares"]:
2180                        info.append(_InfoStr(item))
2181
2182                else:
2183                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2184
2185                # --- Show bonds section:
2186                if view["stat"]["Bonds"]:
2187                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2188
2189                    for item in view["stat"]["Bonds"]:
2190                        info.append(_InfoStr(item))
2191
2192                else:
2193                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2194
2195                # --- Show etfs section:
2196                if view["stat"]["Etfs"]:
2197                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2198
2199                    for item in view["stat"]["Etfs"]:
2200                        info.append(_InfoStr(item))
2201
2202                else:
2203                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2204
2205                # --- Show futures section:
2206                if view["stat"]["Futures"]:
2207                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2208
2209                    for item in view["stat"]["Futures"]:
2210                        info.append(_InfoStr(item))
2211
2212                else:
2213                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2214
2215            if details in ["full", "orders"]:
2216                # --- Show pending orders section:
2217                if view["stat"]["orders"]:
2218                    info.extend([
2219                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2220                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2221                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2222                    ])
2223
2224                    for item in view["stat"]["orders"]:
2225                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2226                            "{} [{}]".format(item["ticker"], item["figi"]),
2227                            item["orderID"],
2228                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2229                            "{} {} ({}{:.2f}%)".format(
2230                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2231                                item["baseCurrencyName"],
2232                                "+" if item["percentChanges"] > 0 else "",
2233                                float(item["percentChanges"]),
2234                            ),
2235                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2236                            item["action"],
2237                            item["type"],
2238                            item["date"],
2239                        ))
2240
2241                else:
2242                    info.append("\n## Total pending limit-orders: 0\n")
2243
2244                # --- Show stop orders section:
2245                if view["stat"]["stopOrders"]:
2246                    info.extend([
2247                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2248                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2249                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2250                    ])
2251
2252                    for item in view["stat"]["stopOrders"]:
2253                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2254                            "{} [{}]".format(item["ticker"], item["figi"]),
2255                            item["orderID"],
2256                            item["lotsRequested"],
2257                            "{} {} ({}{:.2f}%)".format(
2258                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2259                                item["baseCurrencyName"],
2260                                "+" if item["percentChanges"] > 0 else "",
2261                                float(item["percentChanges"]),
2262                            ),
2263                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2264                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2265                            item["action"],
2266                            item["type"],
2267                            item["expType"],
2268                            item["createDate"],
2269                            item["expDate"],
2270                        ))
2271
2272                else:
2273                    info.append("\n## Total stop-orders: 0\n")
2274
2275            if details in ["full", "analytics"]:
2276                # -- Show analytics section:
2277                if view["stat"]["portfolioCostRUB"] > 0:
2278                    info.extend([
2279                        "\n# Analytics\n"
2280                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2281                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2282                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2283                            view["stat"]["totalChangesRUB"],
2284                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2285                            view["stat"]["totalChangesPercentRUB"],
2286                        ),
2287                        "\n## Portfolio distribution by assets\n"
2288                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2289                        "|------------------------------------|---------|---------|--------------------|\n",
2290                    ])
2291
2292                    for key in view["analytics"]["distrByAssets"].keys():
2293                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2294                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2295                                key,
2296                                view["analytics"]["distrByAssets"][key]["uniques"],
2297                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2298                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2299                            ))
2300
2301                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2302
2303                    info.extend([
2304                        "\n## Portfolio distribution by companies\n"
2305                        "\n| Company                                      | Percent | Current cost       |\n",
2306                        aSepLine,
2307                    ])
2308
2309                    for company in view["analytics"]["distrByCompanies"].keys():
2310                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2311                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2312                                "{}{}".format(
2313                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2314                                    company,
2315                                ),
2316                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2317                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2318                            ))
2319
2320                    info.extend([
2321                        "\n## Portfolio distribution by sectors\n"
2322                        "\n| Sector                                       | Percent | Current cost       |\n",
2323                        aSepLine,
2324                    ])
2325
2326                    for sector in view["analytics"]["distrBySectors"].keys():
2327                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2328                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2329                                sector,
2330                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2331                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2332                            ))
2333
2334                    info.extend([
2335                        "\n## Portfolio distribution by currencies\n"
2336                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2337                        aSepLine,
2338                    ])
2339
2340                    for curr in view["analytics"]["distrByCurrencies"].keys():
2341                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2342                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2343                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2344                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2345                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2346                            ))
2347
2348                    info.extend([
2349                        "\n## Portfolio distribution by countries\n"
2350                        "\n| Assets by country                            | Percent | Current cost       |\n",
2351                        aSepLine,
2352                    ])
2353
2354                    for country in view["analytics"]["distrByCountries"].keys():
2355                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2356                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2357                                country,
2358                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2359                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2360                            ))
2361
2362            if details in ["full", "calendar"]:
2363                # -- Show bonds payment calendar section:
2364                if view["stat"]["Bonds"]:
2365                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2366                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2367                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2368
2369                else:
2370                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2371
2372            infoText = "".join(info)
2373
2374            uLogger.info(infoText)
2375
2376            if details == "full" and self.overviewFile:
2377                filename = self.overviewFile
2378
2379            elif details == "digest" and self.overviewDigestFile:
2380                filename = self.overviewDigestFile
2381
2382            elif details == "positions" and self.overviewPositionsFile:
2383                filename = self.overviewPositionsFile
2384
2385            elif details == "orders" and self.overviewOrdersFile:
2386                filename = self.overviewOrdersFile
2387
2388            elif details == "analytics" and self.overviewAnalyticsFile:
2389                filename = self.overviewAnalyticsFile
2390
2391            elif details == "calendar" and self.overviewBondsCalendarFile:
2392                filename = self.overviewBondsCalendarFile
2393
2394            else:
2395                filename = ""
2396
2397            if filename:
2398                with open(filename, "w", encoding="UTF-8") as fH:
2399                    fH.write(infoText)
2400
2401                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2402
2403        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2405    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2406        """
2407        Returns history operations between two given dates for current `accountId`.
2408        If `reportFile` string is not empty then also save human-readable report.
2409        Shows some statistical data of closed positions.
2410
2411        :param start: see docstring in `GetDatesAsString()` method
2412        :param end: see docstring in `GetDatesAsString()` method
2413        :param show: if `True` then also prints all records to the console.
2414        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2415        :return: original list of dictionaries with history of deals records from API ("operations" key):
2416                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2417                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2418        """
2419        if self.accountId is None or not self.accountId:
2420            uLogger.error("Variable `accountId` must be defined for using this method!")
2421            raise Exception("Account ID required")
2422
2423        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2424
2425        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2426
2427        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2428        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2429        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2430        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2431        customStat = {}  # custom statistics in additional to responseJSON
2432
2433        # --- output report in human-readable format:
2434        if show or self.reportFile:
2435            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2436            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2437            nextDay = ""
2438
2439            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2440
2441            if len(ops) > 0:
2442                customStat = {
2443                    "opsCount": 0,  # total operations count
2444                    "buyCount": 0,  # buy operations
2445                    "sellCount": 0,  # sell operations
2446                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2447                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2448                    "payIn": {"rub": 0.},  # Deposit brokerage account
2449                    "payOut": {"rub": 0.},  # Withdrawals
2450                    "divs": {"rub": 0.},  # Dividends income
2451                    "coupons": {"rub": 0.},  # Coupon's income
2452                    "brokerCom": {"rub": 0.},  # Service commissions
2453                    "serviceCom": {"rub": 0.},  # Service commissions
2454                    "marginCom": {"rub": 0.},  # Margin commissions
2455                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2456                }
2457
2458                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2459                for item in ops:
2460                    if item["state"] == "OPERATION_STATE_EXECUTED":
2461                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2462
2463                        # count buy operations:
2464                        if "_BUY" in item["operationType"]:
2465                            customStat["buyCount"] += 1
2466
2467                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2468                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2469
2470                            else:
2471                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2472
2473                        # count sell operations:
2474                        elif "_SELL" in item["operationType"]:
2475                            customStat["sellCount"] += 1
2476
2477                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2478                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2479
2480                            else:
2481                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2482
2483                        # count incoming operations:
2484                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2485                            if item["payment"]["currency"] in customStat["payIn"].keys():
2486                                customStat["payIn"][item["payment"]["currency"]] += payment
2487
2488                            else:
2489                                customStat["payIn"][item["payment"]["currency"]] = payment
2490
2491                        # count withdrawals operations:
2492                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2493                            if item["payment"]["currency"] in customStat["payOut"].keys():
2494                                customStat["payOut"][item["payment"]["currency"]] += payment
2495
2496                            else:
2497                                customStat["payOut"][item["payment"]["currency"]] = payment
2498
2499                        # count dividends income:
2500                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2501                            if item["payment"]["currency"] in customStat["divs"].keys():
2502                                customStat["divs"][item["payment"]["currency"]] += payment
2503
2504                            else:
2505                                customStat["divs"][item["payment"]["currency"]] = payment
2506
2507                        # count coupon's income:
2508                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2509                            if item["payment"]["currency"] in customStat["coupons"].keys():
2510                                customStat["coupons"][item["payment"]["currency"]] += payment
2511
2512                            else:
2513                                customStat["coupons"][item["payment"]["currency"]] = payment
2514
2515                        # count broker commissions:
2516                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2517                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2518                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2519
2520                            else:
2521                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2522
2523                        # count service commissions:
2524                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2525                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2526                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2527
2528                            else:
2529                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2530
2531                        # count margin commissions:
2532                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2533                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2534                                customStat["marginCom"][item["payment"]["currency"]] += payment
2535
2536                            else:
2537                                customStat["marginCom"][item["payment"]["currency"]] = payment
2538
2539                        # count withholding taxes:
2540                        elif "_TAX" in item["operationType"]:
2541                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2542                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2543
2544                            else:
2545                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2546
2547                        else:
2548                            continue
2549
2550                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2551
2552                # --- view "Actions" lines:
2553                info.extend([
2554                    "| Report sections            |                               |                              |                      |                        |\n",
2555                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2556                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2557                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2558                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2559                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2560                    ),
2561                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2562                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2563                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2564                    ),
2565                ])
2566
2567                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2568                for key in opsKeys:
2569                    if key == "rub":
2570                        continue
2571
2572                    info.extend([
2573                        "|                            |                               | {:<28} |                      |                        |\n".format(
2574                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2575                        ),
2576                        "|                            |                               | {:<28} |                      |                        |\n".format(
2577                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2578                        ),
2579                    ])
2580
2581                info.append(splitLine1)
2582
2583                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2584                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2585                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2586                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2587                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2588                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2589                    )
2590
2591                # --- view "Payments" lines:
2592                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2593                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2594
2595                for key in paymentsKeys:
2596                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2597
2598                info.append(splitLine1)
2599
2600                # --- view "Commissions and taxes" lines:
2601                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2602                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2603
2604                for key in comKeys:
2605                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2606
2607                info.append(splitLine1)
2608
2609                info.extend([
2610                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2611                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2612                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2613                ])
2614
2615            else:
2616                info.append("Broker returned no operations during this period\n")
2617
2618            # --- view "Operations" section:
2619            for item in ops:
2620                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2621                    continue
2622
2623                else:
2624                    self.figi = item["figi"] if item["figi"] else ""
2625                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2626                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2627
2628                    # group of deals during one day:
2629                    if nextDay and item["date"].split("T")[0] != nextDay:
2630                        info.append(splitLine2)
2631                        nextDay = ""
2632
2633                    else:
2634                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2635
2636                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2637                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2638                        self.figi if self.figi else "—",
2639                        instrument["ticker"] if instrument else "—",
2640                        instrument["type"] if instrument else "—",
2641                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2642                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2643                        TKS_OPERATION_STATES[item["state"]],
2644                        TKS_OPERATION_TYPES[item["operationType"]],
2645                    ))
2646
2647            infoText = "".join(info)
2648
2649            if show:
2650                if self.moreDebug:
2651                    uLogger.debug("Records about history of a client's operations successfully received")
2652
2653                uLogger.info(infoText)
2654
2655            if self.reportFile:
2656                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2657                    fH.write(infoText)
2658
2659                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2660
2661        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2663    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2664        """
2665        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2666
2667        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2668        Warning! Broker server used ISO UTC time by default.
2669
2670        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2671        Also, `historyFile` used to update history with `onlyMissing` parameter.
2672
2673        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2674
2675        :param start: see docstring in `GetDatesAsString()` method.
2676        :param end: see docstring in `GetDatesAsString()` method.
2677        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2678                         `"hour"`, `"day"`. Default: `"hour"`.
2679        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2680                            False by default. Warning! History appends only from last candle to current time
2681                            with always update last candle!
2682        :param csvSep: separator if csv-file is used, `,` by default.
2683        :param show: if `True` then also prints Pandas DataFrame to the console.
2684        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2685                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2686        """
2687        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2688        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2689        history = None  # empty pandas object for history
2690
2691        if interval not in TKS_CANDLE_INTERVALS.keys():
2692            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2693            raise Exception("Incorrect value")
2694
2695        if not (self.ticker or self.figi):
2696            uLogger.error("Ticker or FIGI must be defined!")
2697            raise Exception("Ticker or FIGI required")
2698
2699        if self.ticker and not self.figi:
2700            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2701            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2702
2703        if self.figi and not self.ticker:
2704            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2705            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2706
2707        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2708        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2709        if interval.lower() != "day":
2710            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2711
2712        delta = dtEnd - dtStart  # current UTC time minus last time in file
2713        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2714
2715        # calculate history length in candles:
2716        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2717        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2718            length += 1  # to avoid fraction time
2719
2720        # calculate data blocks count:
2721        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2722
2723        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2724        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2725        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2726        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2727        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2728
2729        tempOld = None  # pandas object for old history, if --only-missing key present
2730        lastTime = None  # datetime object of last old candle in file
2731
2732        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2733            uLogger.debug("--only-missing key present, add only last missing candles...")
2734            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2735
2736            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2737
2738            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2739            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2740            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2741            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2742
2743            # get last datetime object from last string in file or minus 1 delta if file is empty:
2744            if len(tempOld) > 0:
2745                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2746
2747            else:
2748                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2749
2750            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2751
2752        responseJSONs = []  # raw history blocks of data
2753
2754        blockEnd = dtEnd
2755        for item in range(blocks):
2756            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2757            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2758
2759            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2760                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2761            ))
2762
2763            if blockStart == blockEnd:
2764                uLogger.debug("Skipped this zero-length block...")
2765
2766            else:
2767                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2768                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2769                self.body = str({
2770                    "figi": self.figi,
2771                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2772                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2773                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2774                })
2775                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2776
2777                if "code" in responseJSON.keys():
2778                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2779
2780                else:
2781                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2782                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2783
2784                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2785
2786            blockEnd = blockStart
2787
2788        printCount = len(responseJSONs)  # candles to show in console
2789        if responseJSONs:
2790            tempHistory = pd.DataFrame(
2791                data={
2792                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2793                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2794                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2795                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2796                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2797                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2798                    "volume": [int(item["volume"]) for item in responseJSONs],
2799                },
2800                index=range(len(responseJSONs)),
2801                columns=["date", "time", "open", "high", "low", "close", "volume"],
2802            )
2803            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2804            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2805
2806            # append only newest candles to old history if --only-missing key present:
2807            if onlyMissing and tempOld is not None and lastTime is not None:
2808                index = 0  # find start index in tempHistory data:
2809
2810                for i, item in tempHistory.iterrows():
2811                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2812
2813                    if curTime == lastTime:
2814                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2815                        index = i
2816                        printCount = index + 1
2817                        break
2818
2819                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2820
2821            else:
2822                history = tempHistory  # if no `--only-missing` key then load full data from server
2823
2824            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2825
2826        if history is not None and not history.empty:
2827            if show:
2828                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2829                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2830                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2831                ))
2832
2833        else:
2834            uLogger.warning("Received an empty candles history!")
2835
2836        if self.historyFile is not None:
2837            if history is not None and not history.empty:
2838                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2839                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2840
2841            else:
2842                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2843
2844        else:
2845            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2846
2847        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2849    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2850        """
2851        Load candles history from csv-file and return Pandas DataFrame object.
2852
2853        See also: `History()` and `ShowHistoryChart()` methods.
2854
2855        :param filePath: path to csv-file to open.
2856        """
2857        loadedHistory = None  # init candles data object
2858
2859        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2860
2861        if os.path.exists(filePath):
2862            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2863
2864            tfStr = self.priceModel.FormattedDelta(
2865                self.priceModel.timeframe,
2866                "{days} days {hours}h {minutes}m {seconds}s",
2867            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2868                self.priceModel.timeframe,
2869                "{hours}h {minutes}m {seconds}s",
2870            )
2871
2872            if loadedHistory is not None and not loadedHistory.empty:
2873                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2874                    len(loadedHistory),
2875                    tfStr,
2876                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2877                )
2878
2879            else:
2880                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2881
2882        else:
2883            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2884
2885        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2887    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2888        """
2889        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2890
2891        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2892        Default: `index.html` (both for interact and non-interact candlesticks chart).
2893
2894        See also: `History()` and `LoadHistory()` methods.
2895
2896        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2897        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2898                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2899                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2900                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2901        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2902                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2903        """
2904        if isinstance(candles, str):
2905            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2906            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2907
2908        elif isinstance(candles, pd.DataFrame):
2909            self.priceModel.prices = candles  # set candles chain from variable
2910            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2911
2912            if "datetime" not in candles.columns:
2913                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2914
2915        else:
2916            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2917            raise Exception("Incorrect value")
2918
2919        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2920
2921        if interact:
2922            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2923
2924            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2925
2926        else:
2927            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2928
2929            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2930
2931        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2933    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2934        """
2935        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2936        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2937
2938        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2939
2940        :param operation: string "Buy" or "Sell".
2941        :param lots: volume, integer count of lots >= 1.
2942        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2943        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2944        :param expDate: string "Undefined" by default or local date in future,
2945                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2946        :return: JSON with response from broker server.
2947        """
2948        if self.accountId is None or not self.accountId:
2949            uLogger.error("Variable `accountId` must be defined for using this method!")
2950            raise Exception("Account ID required")
2951
2952        if operation is None or not operation or operation not in ("Buy", "Sell"):
2953            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2954            raise Exception("Incorrect value")
2955
2956        if lots is None or lots < 1:
2957            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2958            lots = 1
2959
2960        if tp is None or tp < 0:
2961            tp = 0
2962
2963        if sl is None or sl < 0:
2964            sl = 0
2965
2966        if expDate is None or not expDate:
2967            expDate = "Undefined"
2968
2969        if not (self.ticker or self.figi):
2970            uLogger.error("Ticker or FIGI must be defined!")
2971            raise Exception("Ticker or FIGI required")
2972
2973        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2974        self.ticker = instrument["ticker"]
2975        self.figi = instrument["figi"]
2976
2977        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2978
2979        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2980        self.body = str({
2981            "figi": self.figi,
2982            "quantity": str(lots),
2983            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2984            "accountId": str(self.accountId),
2985            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2986        })
2987        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2988
2989        if "orderId" in response.keys():
2990            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2991                operation, response["orderId"],
2992                self.ticker, self.figi, lots,
2993                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2994                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2995                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2996            ))
2997
2998            if tp > 0:
2999                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3000
3001            if sl > 0:
3002                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3003
3004        else:
3005            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
3006
3007        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3009    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3010        """
3011        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3012        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3013
3014        See also: `Order()` and `Trade()` docstrings.
3015
3016        :param lots: volume, integer count of lots >= 1.
3017        :param tp: float > 0, take profit price of stop-order.
3018        :param sl: float > 0, stop loss price of stop-order.
3019        :param expDate: it's a local date in future.
3020                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3021        :return: JSON with response from broker server.
3022        """
3023        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3025    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3026        """
3027        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3028        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3029
3030        See also: `Order()` and `Trade()` docstrings.
3031
3032        :param lots: volume, integer count of lots >= 1.
3033        :param tp: float > 0, take profit price of stop-order.
3034        :param sl: float > 0, stop loss price of stop-order.
3035        :param expDate: it's a local date in the future.
3036                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3037        :return: JSON with response from broker server.
3038        """
3039        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3041    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3042        """
3043        Close position of given instruments.
3044
3045        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3046        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3047                         This avoids unnecessary downloading data from the server.
3048        """
3049        if instruments is None or not instruments:
3050            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3051            raise Exception("Ticker or FIGI required")
3052
3053        if isinstance(instruments, str):
3054            instruments = [instruments]
3055
3056        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3057        if uniqueInstruments:
3058            if portfolio is None or not portfolio:
3059                portfolio = self.Overview(show=False)
3060
3061            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3062            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3063
3064            for self.figi in uniqueInstruments:
3065                if self.figi not in allOpened:
3066                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
3067                    continue
3068
3069                # search open trade info about instrument by ticker:
3070                instrument = {}
3071                for iType in TKS_INSTRUMENTS:
3072                    if instrument:
3073                        break
3074
3075                    for item in portfolio["stat"][iType]:
3076                        if item["figi"] == self.figi:
3077                            instrument = item
3078                            break
3079
3080                if instrument:
3081                    self.ticker = instrument["ticker"]
3082                    self.figi = instrument["figi"]
3083
3084                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3085                        self.ticker,
3086                        self.figi,
3087                        int(instrument["volume"]),
3088                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3089                    ))
3090
3091                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3092
3093                    if tradeLots > 0:
3094                        if instrument["blocked"] > 0:
3095                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3096                                instrument["blocked"],
3097                                self.ticker,
3098                                tradeLots,
3099                            ))
3100
3101                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3102                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3103
3104                    else:
3105                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3107    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3108        """
3109        Close all positions of given instruments with defined type.
3110
3111        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3112        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3113                         This avoids unnecessary downloading data from the server.
3114        """
3115        if iType not in TKS_INSTRUMENTS:
3116            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3117
3118        else:
3119            if portfolio is None or not portfolio:
3120                portfolio = self.Overview(show=False)
3121
3122            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3123            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3124
3125            if tickers and portfolio:
3126                self.CloseTrades(tickers, portfolio)
3127
3128            else:
3129                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3131    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3132        """
3133        Universal method to create market or limit orders with all available parameters for current `accountId`.
3134        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3135
3136        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3137        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3138
3139        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3140        then broker immediately open market order as you can do simple --buy or --sell operations!
3141
3142        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3143        When current price will go up or down to target price value then broker opens a limit order.
3144        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3145
3146        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3147
3148        :param operation: string "Buy" or "Sell".
3149        :param orderType: string "Limit" or "Stop".
3150        :param lots: volume, integer count of lots >= 1.
3151        :param targetPrice: target price > 0. This is open trade price for limit order.
3152        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3153                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3154        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3155                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3156                         Stop loss order always executed by market price.
3157        :param expDate: string "Undefined" by default or local date in future.
3158                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3159                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3160                        A limit order has no expiration date, it lasts until the end of the trading day.
3161        :return: JSON with response from broker server.
3162        """
3163        if self.accountId is None or not self.accountId:
3164            uLogger.error("Variable `accountId` must be defined for using this method!")
3165            raise Exception("Account ID required")
3166
3167        if operation is None or not operation or operation not in ("Buy", "Sell"):
3168            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3169            raise Exception("Incorrect value")
3170
3171        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3172            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3173            raise Exception("Incorrect value")
3174
3175        if lots is None or lots < 1:
3176            uLogger.error("You must define trade volume > 0: integer count of lots!")
3177            raise Exception("Incorrect value")
3178
3179        if targetPrice is None or targetPrice <= 0:
3180            uLogger.error("Target price for limit-order must be greater than 0!")
3181            raise Exception("Incorrect value")
3182
3183        if limitPrice is None or limitPrice <= 0:
3184            limitPrice = targetPrice
3185
3186        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3187            stopType = "Limit"
3188
3189        if expDate is None or not expDate:
3190            expDate = "Undefined"
3191
3192        if not (self.ticker or self.figi):
3193            uLogger.error("Tocker or FIGI must be defined!")
3194            raise Exception("Ticker or FIGI required")
3195
3196        response = {}
3197        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3198        self.ticker = instrument["ticker"]
3199        self.figi = instrument["figi"]
3200
3201        if orderType == "Limit":
3202            uLogger.debug(
3203                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3204                    self.ticker, self.figi,
3205                    operation, lots, targetPrice, instrument["currency"],
3206                ))
3207
3208            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3209            self.body = str({
3210                "figi": self.figi,
3211                "quantity": str(lots),
3212                "price": FloatToNano(targetPrice),
3213                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3214                "accountId": str(self.accountId),
3215                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3216            })
3217            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3218
3219            if "orderId" in response.keys():
3220                uLogger.info(
3221                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3222                        response["orderId"],
3223                        self.ticker, self.figi,
3224                        operation, lots, targetPrice, instrument["currency"],
3225                    ))
3226
3227                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3228                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3229                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3230                            targetPrice, instrument["currency"],
3231                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3232                        ))
3233
3234                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3235                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3236                            targetPrice, instrument["currency"],
3237                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3238                        ))
3239
3240            else:
3241                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3242
3243        if orderType == "Stop":
3244            uLogger.debug(
3245                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3246                    self.ticker, self.figi,
3247                    operation, lots,
3248                    targetPrice, instrument["currency"],
3249                    limitPrice, instrument["currency"],
3250                    stopType, expDate,
3251                ))
3252
3253            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3254            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3255            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3256
3257            body = {
3258                "figi": self.figi,
3259                "quantity": str(lots),
3260                "price": FloatToNano(limitPrice),
3261                "stopPrice": FloatToNano(targetPrice),
3262                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3263                "accountId": str(self.accountId),
3264                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3265                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3266            }
3267
3268            if expDateUTC:
3269                body["expireDate"] = expDateUTC
3270
3271            self.body = str(body)
3272            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3273
3274            if "stopOrderId" in response.keys():
3275                uLogger.info(
3276                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3277                        response["stopOrderId"],
3278                        self.ticker, self.figi,
3279                        operation, lots,
3280                        targetPrice, instrument["currency"],
3281                        limitPrice, instrument["currency"],
3282                        TKS_STOP_ORDER_TYPES[stopOrderType],
3283                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3284                    ))
3285
3286                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3287                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3288                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3289                            targetPrice, instrument["currency"],
3290                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3291                        ))
3292
3293                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3294                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3295                            targetPrice, instrument["currency"],
3296                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3297                        ))
3298
3299            else:
3300                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3301
3302        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3304    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3305        """
3306        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3307        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3308        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3309        See also: `Order()` docstring.
3310
3311        :param lots: volume, integer count of lots >= 1.
3312        :param targetPrice: target price > 0. This is open trade price for limit order.
3313        :return: JSON with response from broker server.
3314        """
3315        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3317    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3318        """
3319        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3320        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3321        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3322        target price value then broker opens a limit order. See also: `Order()` docstring.
3323
3324        :param lots: volume, integer count of lots >= 1.
3325        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3326        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3327                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3328        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3329                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3330        :param expDate: string "Undefined" by default or local date in future.
3331                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3332                        This date is converting to UTC format for server.
3333        :return: JSON with response from broker server.
3334        """
3335        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3337    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3338        """
3339        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3340        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3341        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3342        See also: `Order()` docstring.
3343
3344        :param lots: volume, integer count of lots >= 1.
3345        :param targetPrice: target price > 0. This is open trade price for limit order.
3346        :return: JSON with response from broker server.
3347        """
3348        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3350    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3351        """
3352        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3353        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3354        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3355        target price value then broker opens a limit order. See also: `Order()` docstring.
3356
3357        :param lots: volume, integer count of lots >= 1.
3358        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3359        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3360                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3361        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3362                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3363        :param expDate: string "Undefined" by default or local date in future.
3364                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3365                        This date is converting to UTC format for server.
3366        :return: JSON with response from broker server.
3367        """
3368        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3370    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3371        """
3372        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3373
3374        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3375        :param allOrdersIDs: pre-received lists of all active pending orders.
3376                             This avoids unnecessary downloading data from the server.
3377        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3378        """
3379        if self.accountId is None or not self.accountId:
3380            uLogger.error("Variable `accountId` must be defined for using this method!")
3381            raise Exception("Account ID required")
3382
3383        if orderIDs:
3384            if allOrdersIDs is None or not allOrdersIDs:
3385                rawOrders = self.RequestPendingOrders()
3386                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3387
3388            if allStopOrdersIDs is None or not allStopOrdersIDs:
3389                rawStopOrders = self.RequestStopOrders()
3390                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3391
3392            for orderID in orderIDs:
3393                idInPendingOrders = orderID in allOrdersIDs
3394                idInStopOrders = orderID in allStopOrdersIDs
3395
3396                if not (idInPendingOrders or idInStopOrders):
3397                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3398                    continue
3399
3400                else:
3401                    if idInPendingOrders:
3402                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3403
3404                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3405                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3406                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3407                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3408
3409                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3410                            if self.moreDebug:
3411                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3412
3413                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3414
3415                        else:
3416                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3417
3418                    elif idInStopOrders:
3419                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3420
3421                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3422                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3423                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3424                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3425
3426                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3427                            if self.moreDebug:
3428                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3429
3430                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3431
3432                        else:
3433                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3434
3435                    else:
3436                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3438    def CloseAllOrders(self) -> None:
3439        """
3440        Gets a list of open pending and stop orders and cancel it all.
3441        """
3442        rawOrders = self.RequestPendingOrders()
3443        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3444        lenOrders = len(allOrdersIDs)
3445
3446        rawStopOrders = self.RequestStopOrders()
3447        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3448        lenSOrders = len(allStopOrdersIDs)
3449
3450        if lenOrders > 0 or lenSOrders > 0:
3451            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3452
3453            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3454
3455        else:
3456            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3458    def CloseAll(self, *args) -> None:
3459        """
3460        Close all available (not blocked) opened trades and orders.
3461
3462        Also, you can select one or more keywords case-insensitive:
3463        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3464
3465        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3466        """
3467        overview = self.Overview(show=False)  # get all open trades info
3468
3469        if len(args) == 0:
3470            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3471            self.CloseAllOrders()  # close all pending and stop orders
3472
3473            for iType in TKS_INSTRUMENTS:
3474                if iType != "Currencies":
3475                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3476
3477        else:
3478            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3479            lowerArgs = [x.lower() for x in args]
3480
3481            if "orders" in lowerArgs:
3482                self.CloseAllOrders()  # close all pending and stop orders
3483
3484            for iType in TKS_INSTRUMENTS:
3485                if iType.lower() in lowerArgs and iType != "Currencies":
3486                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3488    @staticmethod
3489    def ParseOrderParameters(operation, **inputParameters):
3490        """
3491        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3492
3493        :param operation: string "Buy" or "Sell".
3494        :param inputParameters: this is dict of strings that looks like this
3495               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3496               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3497               "prices" key: one or more prices to open limit-orders
3498               Counts of values in lots and prices lists must be equals!
3499        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3500        """
3501        # TODO: update order grid work with api v2
3502        pass
3503        # uLogger.debug("Input parameters: {}".format(inputParameters))
3504        #
3505        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3506        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3507        #     raise Exception("Incorrect value")
3508        #
3509        # if "l" in inputParameters.keys():
3510        #     inputParameters["lots"] = inputParameters.pop("l")
3511        #
3512        # if "p" in inputParameters.keys():
3513        #     inputParameters["prices"] = inputParameters.pop("p")
3514        #
3515        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3516        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3517        #     raise Exception("Incorrect value")
3518        #
3519        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3520        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3521        #
3522        # if len(lots) != len(prices):
3523        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3524        #     raise Exception("Incorrect value")
3525        #
3526        # uLogger.debug("Extracted parameters for orders:")
3527        # uLogger.debug("lots = {}".format(lots))
3528        # uLogger.debug("prices = {}".format(prices))
3529        #
3530        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3531        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3532        # uLogger.debug("Order parameters: {}".format(result))
3533        #
3534        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3536    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3537        """
3538        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3539
3540        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3541        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3542        """
3543        result = False
3544        msg = "Instrument not defined!"
3545
3546        if portfolio is None or not portfolio:
3547            portfolio = self.Overview(show=False)
3548
3549        if self.ticker:
3550            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3551            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3552
3553            for iType in TKS_INSTRUMENTS:
3554                for instrument in portfolio["stat"][iType]:
3555                    if instrument["ticker"] == self.ticker:
3556                        result = True
3557                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3558                        break
3559
3560        elif self.figi:
3561            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3562            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3563
3564            for iType in TKS_INSTRUMENTS:
3565                for instrument in portfolio["stat"][iType]:
3566                    if instrument["figi"] == self.figi:
3567                        result = True
3568                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3569                        break
3570
3571        else:
3572            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3573
3574        uLogger.debug(msg)
3575
3576        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3578    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3579        """
3580        Returns instrument from the user's portfolio if it presents there.
3581        Instrument must be defined by `ticker` (highly priority) or `figi`.
3582
3583        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3584        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3585        """
3586        result = None
3587        msg = "Instrument not defined!"
3588
3589        if portfolio is None or not portfolio:
3590            portfolio = self.Overview(show=False)
3591
3592        if self.ticker:
3593            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3594            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3595
3596            for iType in TKS_INSTRUMENTS:
3597                for instrument in portfolio["stat"][iType]:
3598                    if instrument["ticker"] == self.ticker:
3599                        result = instrument
3600                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3601                        break
3602
3603        elif self.figi:
3604            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3605            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3606
3607            for iType in TKS_INSTRUMENTS:
3608                for instrument in portfolio["stat"][iType]:
3609                    if instrument["figi"] == self.figi:
3610                        result = instrument
3611                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3612                        break
3613
3614        else:
3615            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3616
3617        uLogger.debug(msg)
3618
3619        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3621    def RequestLimits(self) -> dict:
3622        """
3623        Method for obtaining the available funds for withdrawal for current `accountId`.
3624
3625        See also:
3626        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3627        - `OverviewLimits()` method
3628
3629        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3630                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3631                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3632                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3633        """
3634        if self.accountId is None or not self.accountId:
3635            uLogger.error("Variable `accountId` must be defined for using this method!")
3636            raise Exception("Account ID required")
3637
3638        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3639
3640        self.body = str({"accountId": self.accountId})
3641        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3642        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3643
3644        if self.moreDebug:
3645            uLogger.debug("Records about available funds for withdrawal successfully received")
3646
3647        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3649    def OverviewLimits(self, show: bool = False) -> dict:
3650        """
3651        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3652
3653        See also: `RequestLimits()`.
3654
3655        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3656        :return: dict with raw parsed data from server and some calculated statistics about it.
3657        """
3658        if self.accountId is None or not self.accountId:
3659            uLogger.error("Variable `accountId` must be defined for using this method!")
3660            raise Exception("Account ID required")
3661
3662        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3663
3664        view = {
3665            "rawLimits": rawLimits,
3666            "limits": {  # parsed data for every currency:
3667                "money": {  # this is an array of portfolio currency positions
3668                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3669                },
3670                "blocked": {  # this is an array of blocked currency
3671                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3672                },
3673                "blockedGuarantee": {  # this is locked money under collateral for futures
3674                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3675                },
3676            },
3677        }
3678
3679        # --- Prepare text table with limits in human-readable format:
3680        if show:
3681            info = [
3682                "# Withdrawal limits\n\n",
3683                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3684                "* **Account ID:** [{}]\n".format(self.accountId),
3685            ]
3686
3687            if view["limits"]["money"]:
3688                info.extend([
3689                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3690                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3691                ])
3692
3693            else:
3694                info.append("\nNo withdrawal limits\n")
3695
3696            for curr in view["limits"]["money"].keys():
3697                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3698                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3699                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3700
3701                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3702                    "[{}]".format(curr),
3703                    "{:.2f}".format(view["limits"]["money"][curr]),
3704                    "{:.2f}".format(availableMoney),
3705                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3706                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3707                )
3708
3709                if curr == "rub":
3710                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3711
3712                else:
3713                    info.append(infoStr)
3714
3715            infoText = "".join(info)
3716
3717            uLogger.info(infoText)
3718
3719            if self.withdrawalLimitsFile:
3720                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3721                    fH.write(infoText)
3722
3723                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3724
3725        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3727    def RequestAccounts(self) -> dict:
3728        """
3729        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3730
3731        See also:
3732        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3733        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3734        - `OverviewUserInfo()` method
3735
3736        :return: dict with raw data from server that contains accounts info. Example of dict:
3737                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3738                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3739                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3740                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3741        """
3742        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3743
3744        self.body = str({})
3745        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3746        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3747
3748        if self.moreDebug:
3749            uLogger.debug("Records about available accounts successfully received")
3750
3751        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3753    def RequestUserInfo(self) -> dict:
3754        """
3755        Method for requesting common user's information.
3756
3757        See also:
3758        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3759        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3760        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3761        - `OverviewUserInfo()` method
3762
3763        :return: dict with raw data from server that contains user's information. Example of dict:
3764                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3765                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3766        """
3767        uLogger.debug("Requesting common user's information. Wait, please...")
3768
3769        self.body = str({})
3770        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3771        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3772
3773        if self.moreDebug:
3774            uLogger.debug("Records about current user successfully received")
3775
3776        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3778    def RequestMarginStatus(self, accountId: str = None) -> dict:
3779        """
3780        Method for requesting margin calculation for defined account ID.
3781
3782        See also:
3783        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3784        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3785        - `OverviewUserInfo()` method
3786
3787        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3788        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3789                 Example of responses:
3790                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3791                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3792                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3793                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3794                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3795                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3796        """
3797        if accountId is None or not accountId:
3798            if self.accountId is None or not self.accountId:
3799                uLogger.error("Variable `accountId` must be defined for using this method!")
3800                raise Exception("Account ID required")
3801
3802            else:
3803                accountId = self.accountId  # use `self.accountId` (main ID) by default
3804
3805        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3806
3807        self.body = str({"accountId": accountId})
3808        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3809        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3810
3811        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3812            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3813            rawMargin = {}
3814
3815        else:
3816            if self.moreDebug:
3817                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3818
3819        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3821    def RequestTariffLimits(self) -> dict:
3822        """
3823        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3824
3825        See also:
3826        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3827        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3828        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3829        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3830        - `OverviewUserInfo()` method
3831
3832        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3833                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3834                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3835        """
3836        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3837
3838        self.body = str({})
3839        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3840        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3841
3842        if self.moreDebug:
3843            uLogger.debug("Records with limits of current tariff successfully received")
3844
3845        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3847    def RequestBondCoupons(self, iJSON: dict) -> dict:
3848        """
3849        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3850        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3851        All dates are in UTC timezone.
3852
3853        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3854        Documentation:
3855        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3856        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3857
3858        See also: `ExtendBondsData()`.
3859
3860        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3861                      If raw iJSON is not data of bond then server returns an error [400] with message:
3862                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3863        :return: dictionary with bond payment calendar. Response example
3864                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3865                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3866                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3867                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3868        """
3869        if iJSON["figi"] is None or not iJSON["figi"]:
3870            uLogger.error("FIGI must be defined for using this method!")
3871            raise Exception("FIGI required")
3872
3873        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3874        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3875
3876        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3877            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3878            self.figi,
3879            startDate,
3880            endDate,
3881        ))
3882
3883        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3884        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3885        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3886
3887        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3888            uLogger.warning("Instrument type is not bond!")
3889
3890        else:
3891            if self.moreDebug:
3892                uLogger.debug("Records about bond payment calendar successfully received")
3893
3894        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3896    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3897        """
3898        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3899        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3900        coupon yields, current yields and some statistics etc.
3901
3902        WARNING! This is too long operation if a lot of bonds requested from broker server.
3903
3904        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3905
3906        :param instruments: list of strings with tickers or FIGIs.
3907        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3908                     for further used by data scientists or stock analytics.
3909        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3910                 In XLSX-file and Pandas DataFrame fields mean:
3911                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3912                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3913        """
3914        if instruments is None or not instruments:
3915            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3916            raise Exception("Ticker or FIGI required")
3917
3918        if isinstance(instruments, str):
3919            instruments = [instruments]
3920
3921        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3922
3923        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3924
3925        iCount = len(uniqueInstruments)
3926        tooLong = iCount >= 20
3927        if tooLong:
3928            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3929
3930        bonds = None
3931        for i, self.figi in enumerate(uniqueInstruments):
3932            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3933
3934            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3935                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3936                rawBond = self.SearchByFIGI(requestPrice=True)
3937
3938                # Widen raw data with UTC current time (iData["actualDateTime"]):
3939                actualDate = datetime.now(tzutc())
3940                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3941
3942                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3943                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3944
3945                # Replace some values with human-readable:
3946                iData["nominalCurrency"] = iData["nominal"]["currency"]
3947                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3948                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3949                iData["aciCurrency"] = iData["aciValue"]["currency"]
3950                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3951                iData["issueSize"] = int(iData["issueSize"])
3952                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3953                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3954                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3955                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3956                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3957                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3958                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3959                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3960                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3961                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3962
3963                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3964                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3965                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3966                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3967                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3968                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3969                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3970                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3971                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3972                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3973                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3974
3975                # Widen raw data with calendar data from `rawCalendar` values:
3976                calendarData = []
3977                if "events" in iData["rawCalendar"].keys():
3978                    for item in iData["rawCalendar"]["events"]:
3979                        calendarData.append({
3980                            "couponDate": item["couponDate"],
3981                            "couponNumber": int(item["couponNumber"]),
3982                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3983                            "payCurrency": item["payOneBond"]["currency"],
3984                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3985                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3986                            "couponStartDate": item["couponStartDate"],
3987                            "couponEndDate": item["couponEndDate"],
3988                            "couponPeriod": item["couponPeriod"],
3989                        })
3990
3991                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3992                    if "maturityDate" not in iData.keys():
3993                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3994
3995                # Widen raw data with Coupon Rate.
3996                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3997                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3998                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3999                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4000
4001                # Widen raw data with Yield to Maturity (YTM) on current date.
4002                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4003                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4004                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4005                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4006                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4007                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4008
4009                iData["calendar"] = calendarData  # adds calendar at the end
4010
4011                # Remove not used data:
4012                iData.pop("uid")
4013                iData.pop("positionUid")
4014                iData.pop("currentPrice")
4015                iData.pop("rawCalendar")
4016
4017                colNames = list(iData.keys())
4018                if bonds is None:
4019                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4020
4021                else:
4022                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4023
4024            else:
4025                uLogger.warning("Instrument is not a bond!")
4026
4027            processed = round(100 * (i + 1) / iCount, 1)
4028            if tooLong and processed % 5 == 0:
4029                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4030
4031            else:
4032                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4033
4034        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4035
4036        # Saving bonds from Pandas DataFrame to XLSX sheet:
4037        if xlsx and self.bondsXLSXFile:
4038            with pd.ExcelWriter(
4039                    path=self.bondsXLSXFile,
4040                    date_format=TKS_DATE_FORMAT,
4041                    datetime_format=TKS_DATE_TIME_FORMAT,
4042                    mode="w",
4043            ) as writer:
4044                bonds.to_excel(
4045                    writer,
4046                    sheet_name="Extended bonds data",
4047                    index=True,
4048                    encoding="UTF-8",
4049                    freeze_panes=(1, 1),
4050                )  # saving as XLSX-file with freeze first row and column as headers
4051
4052            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4053
4054        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4056    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4057        """
4058        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4059
4060        WARNING! This is too long operation if a lot of bonds requested from broker server.
4061
4062        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4063
4064        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4065                        extended information about bonds: main info, current prices, bond payment calendar,
4066                        coupon yields, current yields and some statistics etc.
4067                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4068        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4069                     for further used by data scientists or stock analytics.
4070        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4071        """
4072        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4073            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4074
4075        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4076
4077        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4078        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4079        calendar = None
4080        for bond in extBonds.iterrows():
4081            for item in bond[1]["calendar"]:
4082                cData = {
4083                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4084                    "couponDate": item["couponDate"],
4085                    "figi": bond[1]["figi"],
4086                    "ticker": bond[1]["ticker"],
4087                    "name": bond[1]["name"],
4088                    "couponNumber": item["couponNumber"],
4089                    "payOneBond": item["payOneBond"],
4090                    "payCurrency": item["payCurrency"],
4091                    "couponType": item["couponType"],
4092                    "couponPeriod": item["couponPeriod"],
4093                    "fixDate": item["fixDate"],
4094                    "couponStartDate": item["couponStartDate"],
4095                    "couponEndDate": item["couponEndDate"],
4096                }
4097
4098                if calendar is None:
4099                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4100
4101                else:
4102                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4103
4104        if calendar is not None:
4105            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4106
4107            # Saving calendar from Pandas DataFrame to XLSX sheet:
4108            if xlsx:
4109                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4110
4111                with pd.ExcelWriter(
4112                        path=xlsxCalendarFile,
4113                        date_format=TKS_DATE_FORMAT,
4114                        datetime_format=TKS_DATE_TIME_FORMAT,
4115                        mode="w",
4116                ) as writer:
4117                    humanReadable = calendar.copy(deep=True)
4118                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4119                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4120                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4121                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4122                    humanReadable.columns = colNames  # human-readable column names
4123
4124                    humanReadable.to_excel(
4125                        writer,
4126                        sheet_name="Bond payments calendar",
4127                        index=False,
4128                        encoding="UTF-8",
4129                        freeze_panes=(1, 2),
4130                    )  # saving as XLSX-file with freeze first row and column as headers
4131
4132                    del humanReadable  # release df in memory
4133
4134                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4135
4136        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4138    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4139        """
4140        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4141        Also, creates Markdown file with calendar data, `calendar.md` by default.
4142
4143        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4144
4145        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4146                        extended information about bonds: main info, current prices, bond payment calendar,
4147                        coupon yields, current yields and some statistics etc.
4148                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4149        :param show: if `True` then also printing bonds payment calendar to the console,
4150                     otherwise save to file `calendarFile` only. `False` by default.
4151        :return: multilines text in Markdown format with bonds payment calendar as a table.
4152        """
4153        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4154            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4155
4156        infoText = "# Bond payments calendar\n\n"
4157
4158        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4159
4160        if not (calendar is None or calendar.empty):
4161            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4162
4163            info = [
4164                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4165                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4166            ]
4167
4168            newMonth = False
4169            notOneBond = calendar["figi"].nunique() > 1
4170            for i, bond in enumerate(calendar.iterrows()):
4171                if newMonth and notOneBond:
4172                    info.append(splitLine)
4173
4174                info.append(
4175                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4176                        "  √" if bond[1]["paid"] else "  —",
4177                        bond[1]["couponDate"].split("T")[0],
4178                        bond[1]["figi"],
4179                        bond[1]["ticker"],
4180                        bond[1]["couponNumber"],
4181                        "{} {}".format(
4182                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4183                            bond[1]["payCurrency"],
4184                        ),
4185                        bond[1]["couponType"],
4186                        bond[1]["couponPeriod"],
4187                        bond[1]["fixDate"].split("T")[0],
4188                    )
4189                )
4190
4191                if i < len(calendar.values) - 1:
4192                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4193                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4194                    newMonth = False if curDate.month == nextDate.month else True
4195
4196                else:
4197                    newMonth = False
4198
4199            infoText += "".join(info)
4200
4201            if show:
4202                uLogger.info("{}".format(infoText))
4203
4204            if self.calendarFile is not None:
4205                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4206                    fH.write(infoText)
4207
4208                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4209
4210        else:
4211            infoText += "No data\n"
4212
4213        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4215    def OverviewAccounts(self, show: bool = False) -> dict:
4216        """
4217        Method for parsing and show simple table with all available user accounts.
4218
4219        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4220
4221        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4222        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4223                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4224                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4225                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4226                                                        "closed": "—", "access": "Full access" }, ...}}`
4227        """
4228        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4229
4230        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4231        accounts = {
4232            item["id"]: {
4233                "type": TKS_ACCOUNT_TYPES[item["type"]],
4234                "name": item["name"],
4235                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4236                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4237                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4238                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4239            } for item in rawAccounts["accounts"]
4240        }
4241
4242        # Raw and parsed data with some fields replaced in "stat" section:
4243        view = {
4244            "rawAccounts": rawAccounts,
4245            "stat": accounts,
4246        }
4247
4248        # --- Prepare simple text table with only accounts data in human-readable format:
4249        if show:
4250            info = [
4251                "# User accounts\n\n",
4252                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4253                "| Account ID   | Type                      | Status                    | Name                           |\n",
4254                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4255            ]
4256
4257            for account in view["stat"].keys():
4258                info.extend([
4259                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4260                        account,
4261                        view["stat"][account]["type"],
4262                        view["stat"][account]["status"],
4263                        view["stat"][account]["name"],
4264                    )
4265                ])
4266
4267            infoText = "".join(info)
4268
4269            uLogger.info(infoText)
4270
4271            if self.userAccountsFile:
4272                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4273                    fH.write(infoText)
4274
4275                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4276
4277        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4279    def OverviewUserInfo(self, show: bool = False) -> dict:
4280        """
4281        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4282
4283        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4284
4285        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4286        :return: dict with raw parsed data from server and some calculated statistics about it.
4287        """
4288        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4289        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4290        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4291        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4292        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4293        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4294
4295        # This is dict with parsed common user data:
4296        userInfo = {
4297            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4298            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4299            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4300            "tariff": rawUserInfo["tariff"],
4301        }
4302
4303        # This is an array of dict with parsed margin statuses for every account IDs:
4304        margins = {}
4305        for accountId in accounts.keys():
4306            if rawMargins[accountId]:
4307                margins[accountId] = {
4308                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4309                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4310                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4311                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4312                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4313                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4314                }
4315
4316            else:
4317                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4318
4319        unary = {}  # unary-connection limits
4320        for item in rawTariffLimits["unaryLimits"]:
4321            if item["limitPerMinute"] in unary.keys():
4322                unary[item["limitPerMinute"]].extend(item["methods"])
4323
4324            else:
4325                unary[item["limitPerMinute"]] = item["methods"]
4326
4327        stream = {}  # stream-connection limits
4328        for item in rawTariffLimits["streamLimits"]:
4329            if item["limit"] in stream.keys():
4330                stream[item["limit"]].extend(item["streams"])
4331
4332            else:
4333                stream[item["limit"]] = item["streams"]
4334
4335        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4336        limits = {
4337            "unary": unary,
4338            "stream": stream,
4339        }
4340
4341        # Raw and parsed data as an output result:
4342        view = {
4343            "rawUserInfo": rawUserInfo,
4344            "rawAccounts": rawAccounts,
4345            "rawMargins": rawMargins,
4346            "rawTariffLimits": rawTariffLimits,
4347            "stat": {
4348                "userInfo": userInfo,
4349                "accounts": accounts,
4350                "margins": margins,
4351                "limits": limits,
4352            },
4353        }
4354
4355        # --- Prepare text table with user information in human-readable format:
4356        if show:
4357            info = [
4358                "# Full user information\n\n",
4359                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4360                "## Common information\n\n",
4361                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4362                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4363                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4364                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4365                "\n## User accounts\n\n",
4366            ]
4367
4368            for account in view["stat"]["accounts"].keys():
4369                info.extend([
4370                    "### ID: [{}]\n\n".format(account),
4371                    "| Parameters           | Values                                                       |\n",
4372                    "|----------------------|--------------------------------------------------------------|\n",
4373                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4374                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4375                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4376                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4377                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4378                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4379                ])
4380
4381                if margins[account]:
4382                    info.extend([
4383                        "| Margin status:       | Enabled                                                      |\n",
4384                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4385                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4386                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4387                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4388                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4389                    ])
4390
4391                else:
4392                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4393
4394            info.extend([
4395                "\n## Current user tariff limits\n",
4396                "\nSee also:\n",
4397                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4398                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4399                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4400                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4401                "\n### Unary limits\n",
4402            ])
4403
4404            if unary:
4405                for key, values in sorted(unary.items()):
4406                    info.append("\n* Max requests per minute: {}\n".format(key))
4407
4408                    for value in values:
4409                        info.append("  - {}\n".format(value))
4410
4411            else:
4412                info.append("\nNot available\n")
4413
4414            info.append("\n### Stream limits\n")
4415
4416            if stream:
4417                for key, values in sorted(stream.items()):
4418                    info.append("\n* Max stream connections: {}\n".format(key))
4419
4420                    for value in values:
4421                        info.append("  - {}\n".format(value))
4422
4423            else:
4424                info.append("\nNot available\n")
4425
4426            infoText = "".join(info)
4427
4428            uLogger.info(infoText)
4429
4430            if self.userInfoFile:
4431                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4432                    fH.write(infoText)
4433
4434                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4435
4436        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4439class Args:
4440    """
4441    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4442    """
4443    def __init__(self, **kwargs):
4444        self.__dict__.update(kwargs)
4445
4446    def __getattr__(self, item):
4447        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4443    def __init__(self, **kwargs):
4444        self.__dict__.update(kwargs)
def ParseArgs()
4450def ParseArgs():
4451    """This function get and parse command line keys."""
4452    parser = ArgumentParser()  # command-line string parser
4453
4454    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4455    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4456
4457    # --- options:
4458
4459    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4460    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4461    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4462
4463    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4464    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4465
4466    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4467    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4468
4469    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4470
4471    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4472    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4473    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4474
4475    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4476    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4477
4478    # --- commands:
4479
4480    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4481
4482    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4483    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4484    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4485    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4486    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4487    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4488    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4489    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4490
4491    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4492    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4493    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4494    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4495    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4496    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4497
4498    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4499    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4500    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4501    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4502
4503    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4504    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4505    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4506
4507    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4508    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4509    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4510    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4511    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4512    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4513    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4514
4515    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4516    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4517    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4518    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4519    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4520
4521    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4522    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4523    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4524
4525    cmdArgs = parser.parse_args()
4526    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4529def Main(**kwargs):
4530    """
4531    Main function for work with TKSBrokerAPI in the console.
4532
4533    See examples:
4534    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4535    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4536    """
4537    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4538
4539    if args.debug_level:
4540        uLogger.level = 10  # always debug level by default
4541        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4542
4543    exitCode = 0
4544    start = datetime.now(tzutc())
4545    uLogger.debug("=-" * 50)
4546    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4547        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4548        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4549    ))
4550
4551    # trying to calculate full current version:
4552    buildVersion = __version__
4553    try:
4554        v = version("tksbrokerapi")
4555        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4556
4557    except Exception:
4558        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4559
4560    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4561    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4562
4563    try:
4564        if args.version:
4565            print("TKSBrokerAPI {}".format(buildVersion))
4566            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4567
4568        else:
4569            # Init class for trading with Tinkoff Broker:
4570            trader = TinkoffBrokerServer(
4571                token=args.token,
4572                accountId=args.account_id,
4573                useCache=not args.no_cache,
4574            )
4575
4576            # --- set some options:
4577
4578            if args.more:
4579                trader.moreDebug = True
4580                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4581
4582            if args.ticker:
4583                ticker = args.ticker.upper()  # Tickers may be upper case only
4584
4585                if ticker in trader.aliasesKeys:
4586                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4587
4588                else:
4589                    trader.ticker = ticker
4590
4591            if args.figi:
4592                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4593
4594            if args.depth is not None:
4595                trader.depth = args.depth
4596
4597            # --- do one command:
4598
4599            if args.list:
4600                if args.output is not None:
4601                    trader.instrumentsFile = args.output
4602
4603                trader.ShowInstrumentsInfo(show=True)
4604
4605            elif args.list_xlsx:
4606                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4607
4608            elif args.bonds_xlsx is not None:
4609                if args.output is not None:
4610                    trader.bondsXLSXFile = args.output
4611
4612                if len(args.bonds_xlsx) == 0:
4613                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4614
4615                else:
4616                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4617
4618            elif args.search:
4619                if args.output is not None:
4620                    trader.searchResultsFile = args.output
4621
4622                trader.SearchInstruments(pattern=args.search[0], show=True)
4623
4624            elif args.info:
4625                if not (args.ticker or args.figi):
4626                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4627                    raise Exception("Ticker or FIGI required")
4628
4629                if args.output is not None:
4630                    trader.infoFile = args.output
4631
4632                if args.ticker:
4633                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4634
4635                else:
4636                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4637
4638            elif args.calendar is not None:
4639                if args.output is not None:
4640                    trader.calendarFile = args.output
4641
4642                if len(args.calendar) == 0:
4643                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4644
4645                else:
4646                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4647
4648                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4649
4650            elif args.price:
4651                if not (args.ticker or args.figi):
4652                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4653                    raise Exception("Ticker or FIGI required")
4654
4655                trader.GetCurrentPrices(show=True)
4656
4657            elif args.prices is not None:
4658                if args.output is not None:
4659                    trader.pricesFile = args.output
4660
4661                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4662
4663            elif args.overview:
4664                if args.output is not None:
4665                    trader.overviewFile = args.output
4666
4667                trader.Overview(show=True, details="full")
4668
4669            elif args.overview_digest:
4670                if args.output is not None:
4671                    trader.overviewDigestFile = args.output
4672
4673                trader.Overview(show=True, details="digest")
4674
4675            elif args.overview_positions:
4676                if args.output is not None:
4677                    trader.overviewPositionsFile = args.output
4678
4679                trader.Overview(show=True, details="positions")
4680
4681            elif args.overview_orders:
4682                if args.output is not None:
4683                    trader.overviewOrdersFile = args.output
4684
4685                trader.Overview(show=True, details="orders")
4686
4687            elif args.overview_analytics:
4688                if args.output is not None:
4689                    trader.overviewAnalyticsFile = args.output
4690
4691                trader.Overview(show=True, details="analytics")
4692
4693            elif args.overview_calendar:
4694                if args.output is not None:
4695                    trader.overviewAnalyticsFile = args.output
4696
4697                trader.Overview(show=True, details="calendar")
4698
4699            elif args.deals is not None:
4700                if args.output is not None:
4701                    trader.reportFile = args.output
4702
4703                if 0 <= len(args.deals) < 3:
4704                    trader.Deals(
4705                        start=args.deals[0] if len(args.deals) >= 1 else None,
4706                        end=args.deals[1] if len(args.deals) == 2 else None,
4707                        show=True,  # Always show deals report in console
4708                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4709                    )
4710
4711                else:
4712                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4713                    raise Exception("Incorrect value")
4714
4715            elif args.history is not None:
4716                if args.output is not None:
4717                    trader.historyFile = args.output
4718
4719                if 0 <= len(args.history) < 3:
4720                    dataReceived = trader.History(
4721                        start=args.history[0] if len(args.history) >= 1 else None,
4722                        end=args.history[1] if len(args.history) == 2 else None,
4723                        interval="hour" if args.interval is None or not args.interval else args.interval,
4724                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4725                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4726                        show=True,  # shows all downloaded candles in console
4727                    )
4728
4729                    if args.render_chart is not None and dataReceived is not None:
4730                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4731
4732                        trader.ShowHistoryChart(
4733                            candles=dataReceived,
4734                            interact=iChart,
4735                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4736                        )
4737
4738                else:
4739                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4740                    raise Exception("Incorrect value")
4741
4742            elif args.load_history is not None:
4743                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4744
4745                if args.render_chart is not None and histData is not None:
4746                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4747                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4748
4749                    trader.ShowHistoryChart(
4750                        candles=histData,
4751                        interact=iChart,
4752                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4753                    )
4754
4755            elif args.trade is not None:
4756                if 1 <= len(args.trade) <= 5:
4757                    trader.Trade(
4758                        operation=args.trade[0],
4759                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4760                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4761                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4762                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4763                    )
4764
4765                else:
4766                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4767
4768            elif args.buy is not None:
4769                if 0 <= len(args.buy) <= 4:
4770                    trader.Buy(
4771                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4772                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4773                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4774                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4775                    )
4776
4777                else:
4778                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4779
4780            elif args.sell is not None:
4781                if 0 <= len(args.sell) <= 4:
4782                    trader.Sell(
4783                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4784                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4785                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4786                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4787                    )
4788
4789                else:
4790                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4791
4792            elif args.order:
4793                if 4 <= len(args.order) <= 7:
4794                    trader.Order(
4795                        operation=args.order[0],
4796                        orderType=args.order[1],
4797                        lots=int(args.order[2]),
4798                        targetPrice=float(args.order[3]),
4799                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4800                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4801                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4802                    )
4803
4804                else:
4805                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4806
4807            elif args.buy_limit:
4808                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4809
4810            elif args.sell_limit:
4811                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4812
4813            elif args.buy_stop:
4814                if 2 <= len(args.buy_stop) <= 7:
4815                    trader.BuyStop(
4816                        lots=int(args.buy_stop[0]),
4817                        targetPrice=float(args.buy_stop[1]),
4818                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4819                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4820                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4821                    )
4822
4823                else:
4824                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4825
4826            elif args.sell_stop:
4827                if 2 <= len(args.sell_stop) <= 7:
4828                    trader.SellStop(
4829                        lots=int(args.sell_stop[0]),
4830                        targetPrice=float(args.sell_stop[1]),
4831                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4832                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4833                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4834                    )
4835
4836                else:
4837                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4838
4839            # elif args.buy_order_grid is not None:
4840            #     # update order grid work with api v2
4841            #     if len(args.buy_order_grid) == 2:
4842            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4843            #
4844            #         for order in orderParams:
4845            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4846            #
4847            #     else:
4848            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4849            #
4850            # elif args.sell_order_grid is not None:
4851            #     # update order grid work with api v2
4852            #     if len(args.sell_order_grid) >= 2:
4853            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4854            #
4855            #         for order in orderParams:
4856            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4857            #
4858            #     else:
4859            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4860
4861            elif args.close_order is not None:
4862                trader.CloseOrders(args.close_order)  # close only one order
4863
4864            elif args.close_orders is not None:
4865                trader.CloseOrders(args.close_orders)  # close list of orders
4866
4867            elif args.close_trade:
4868                if not (args.ticker or args.figi):
4869                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4870                    raise Exception("Ticker or FIGI required")
4871
4872                if args.ticker:
4873                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4874
4875                else:
4876                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4877
4878            elif args.close_trades is not None:
4879                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4880
4881            elif args.close_all is not None:
4882                trader.CloseAll(*args.close_all)
4883
4884            elif args.limits:
4885                if args.output is not None:
4886                    trader.withdrawalLimitsFile = args.output
4887
4888                trader.OverviewLimits(show=True)
4889
4890            elif args.user_info:
4891                if args.output is not None:
4892                    trader.userInfoFile = args.output
4893
4894                trader.OverviewUserInfo(show=True)
4895
4896            elif args.account:
4897                if args.output is not None:
4898                    trader.userAccountsFile = args.output
4899
4900                trader.OverviewAccounts(show=True)
4901
4902            else:
4903                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4904                raise Exception("There is no command to execute")
4905
4906    except Exception:
4907        trace = tb.format_exc()
4908        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4909            if e in trace:
4910                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4911                break
4912
4913        uLogger.debug(trace)
4914        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4915        exitCode = 255  # an error occurred, must be open a ticket for this issue
4916
4917    finally:
4918        finish = datetime.now(tzutc())
4919
4920        if exitCode == 0:
4921            if args.more:
4922                uLogger.debug("All operations were finished success (summary code is 0).")
4923
4924        else:
4925            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4926                os.path.abspath(uLog.defaultLogFile), exitCode,
4927            ))
4928
4929        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4930        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4931            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4932            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4933        ))
4934        uLogger.debug("=-" * 50)
4935
4936        if not kwargs:
4937            sys.exit(exitCode)
4938
4939        else:
4940            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: